Skip to content

feat: add portable CLI binaries for multi-platform Supabase CLI distribution#2033

Open
LGUG2Z wants to merge 1 commit intodevelopfrom
feature/sb-cli-binaries
Open

feat: add portable CLI binaries for multi-platform Supabase CLI distribution#2033
LGUG2Z wants to merge 1 commit intodevelopfrom
feature/sb-cli-binaries

Conversation

@LGUG2Z
Copy link

@LGUG2Z LGUG2Z commented Feb 2, 2026

What kind of change does this PR introduce?

Adds a new, portable psql_17_cli_portable target.

What is the current behavior?

All current builds are not portable (i.e. they have hard dependencies on the /nix/store), and they all include the full list of extensions.

What is the new behavior?

The psql_17_cli_portable target can run without a /nix/store present, and only includes the supautils extension.

Additional context

This commit implements portable, self-contained PostgreSQL binaries for
the Supabase CLI across macOS (ARM), Linux (x64), and Linux (ARM64),
along with automated CI/CD workflows for building and releasing these
artifacts.

The Supabase CLI needs to ship PostgreSQL binaries that work on user
machines without requiring Nix or other system dependencies. This means
extracting the actual binaries from Nix's wrapper scripts, bundling all
necessary shared libraries, and patching them to use relative paths
instead of hardcoded Nix store paths.

A `variant` parameter was added to the postgres build pipeline to
distinguish between "full" (all extensions) and "cli" (minimal
extensions for Supabase CLI). The `cliExtensions` list contains 6
extensions required for running Supabase migrations: supautils,
pg_graphql, pgsodium, supabase_vault, pg_net, and pg_cron. Built-in
extensions (uuid-ossp, pgcrypto, pg_stat_statements) are included
automatically with PostgreSQL. `makeOurPostgresPkgs`/`makePostgresBin`
were modified to accept this parameter. A new `psql_17_cli` package is
created using `variant = "cli"`, while the full extension set is
preserved for base packages (`psql_15`, `psql_17`, `psql_orioledb-17`).

The portable CLI variant (`psql_17_cli_portable`) includes 6 extensions
for migration support while maintaining a significantly smaller size
than the full build. The implementation in
`nix/packages/postgres-portable.nix` extracts binaries from
`psql_17_cli` using a `resolve_binary()` function that follows wrapper
layers to find the actual ELF/Mach-O binaries behind Nix's environment
setup scripts.

All Nix-provided libraries (ICU, readline, zlib, etc.) are bundled while
excluding system libraries (`libc`, `libpthread`, `libm`, `glibc`,
`libdl`) that must come from the host. This distinction is critical:
Linux bundles must exclude glibc due to kernel ABI dependencies, while
macOS can include more libs due to its different linking model.
Dependency resolution runs multiple passes to catch transitive deps
(e.g., ICU → charset → etc.).

Platform-specific patching is applied: Linux binaries use the system
interpreter (`/lib64/ld-linux-*.so.2`) and `$ORIGIN`-based RPATHs, while
macOS binaries use `@rpath` with `@executable_path`. Wrapper scripts set
`LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS) to find bundled
libraries.

The bundle includes PostgreSQL config templates (`postgresql.conf`,
`pg_hba.conf`, `pg_ident.conf`) tailored for CLI usage with minimal
local dev settings, plus the complete Supabase migration script
(`migrate.sh`) with all init-scripts and migration SQL files (55 files,
236KB).

To achieve true portability, a `portable` parameter was added to the
PostgreSQL build in `nix/postgresql/generic.nix`. When `portable =
true`, three critical hardcoded paths are excluded from the build:

1. `--with-system-tzdata` is removed from configure flags, allowing
   PostgreSQL to use bundled timezone data from the `share/` directory
   instead of a hardcoded `/nix/store/.../tzdata` path;

2. the `locale-binary-path.patch` is excluded, so PostgreSQL calls
   `locale -a` from system PATH rather than using an absolute path to
   glibc's locale command;

3. the `postFixup` initdb wrapper is disabled to avoid hardcoding glibc
   paths. The `portable` parameter defaults to `false` in
   `nix/postgresql/default.nix` but is overridden to `true` for the CLI
   variant in `nix/packages/postgres.nix` using `.override { portable =
   true; }`.

This ensures standard PostgreSQL builds remain unchanged while the CLI
variant produces truly portable binaries without any `/nix/store`
references.

A GitHub Actions workflow builds portable binaries across all three
platforms using a matrix strategy. Each build runs automated portability
checks that verify no `/nix/store` references remain, validate RPATH
configuration, confirm transitive dependencies are bundled, ensure
system libraries are NOT bundled, and check wrapper scripts contain
proper library path setup.

Post-build testing validates binaries work without Nix (`postgres
--version`, `psql --version`), that Supabase migrations can run
successfully, and that tzdata is available.

On tagged releases (`v*-cli`), the workflow creates GitHub releases with
tarball artifacts and checksums.

The test infrastructure needed significant changes to support variants
with different extension sets. An `isCliVariant` parameter was added to
`makeCheckHarness`, and the hardcoded `shared_preload_libraries` list in
`postgresql.conf.in` was replaced with a `@PRELOAD_LIBRARIES@`
placeholder. A `generatePreloadLibs` script now parses `receipt.json` at
test time and dynamically builds the preload list based on available
extensions, removing the previous timescaledb removal hack for OrioleDB.

Summary by CodeRabbit

  • New Features
    • Portable, multi-architecture PostgreSQL CLI bundles and a CLI runtime variant; lightweight local Postgres config/auth templates, CLI init SQL, and a pgsodium key-generation helper.
  • Tests
    • CLI-specific init/test flow, dynamic preload library handling, and an extensive portability test harness validating binaries, libraries, tzdata, and extension/migration behavior.
  • Infrastructure
    • CI workflow to build per-architecture bundles, run portability checks, package tarballs with checksums, and publish GitHub Releases.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch 30 times, most recently from cebd363 to 57718b4 Compare February 3, 2026 20:13
@jgoux
Copy link
Contributor

jgoux commented Feb 4, 2026

Here are the issues I had integrating the artefact into the CLI:

PostgreSQL Bundle Issues

This document describes the issues encountered with the current PostgreSQL bundle (supabase-postgres-<version>-<os>-<arch>.zip) and the workarounds implemented in the CLI.

Goal: The bundle should be extractable and runnable without any CLI-side workarounds.


1. Code Signing on macOS (Critical)

Status: Blocking - requires CLI workaround

Issue: After extracting the zip, all binaries crash immediately with:

SIGKILL (Code Signature Invalid)
termination: {"flags":0,"code":2,"namespace":"CODESIGNING","indicator":"Invalid Page"}

Root Cause: The binaries have invalid or missing code signatures after extraction. macOS's security system (Gatekeeper) kills unsigned/invalid binaries.

Current Workaround: Re-sign all executables and .dylib files with ad-hoc signature after extraction:

// binary.go:434-463 - codesignPostgresDir()
cmd := exec.Command("codesign", "-f", "-s", "-", path)

Ideal Fix: Sign binaries during packaging, or use proper Mach-O flags that don't require signing.


2. Timezone Data - Hardcoded Nix Paths (Critical)

Status: Blocking PostgREST - no effective workaround

Issue: The postgres binary was built with:

--with-system-tzdata=/nix/store/fy3qa8s8kzb7a6abzmyidzp1c8axz3s3-tzdata-2025b/share/zoneinfo

This causes pg_timezone_names to fail:

ERROR: could not open directory "/nix/store/fy3qa8s8kzb7a6abzmyidzp1c8axz3s3-tzdata-2025b/share/zoneinfo": No such file or directory

Impact:

  • PostgREST cannot load schema cache (calls pg_timezone_names during introspection)
  • Any query involving timezone enumeration fails

Attempted Workarounds:

  1. Setting TZDIR=/var/db/timezone/zoneinfo environment variable - doesn't work for pg_timezone_names
  2. Creating symlink at /nix/store/... - requires root privileges

Ideal Fix: Either:

  • Build postgres WITHOUT --with-system-tzdata (use bundled tzdata)
  • Include tzdata in the bundle at a relative path
  • Build with relocatable paths

3. safeupdate Extension Missing

Status: Fixed with CLI workaround

Issue: The bundled migrations configure the authenticator role with:

session_preload_libraries=safeupdate

But the safeupdate extension is not included in the bundle, causing connection failures:

FATAL: could not access file "safeupdate": No such file or directory

Current Workaround: Reset the setting after migrations run:

-- process-compose.yaml.tmpl:56-57
ALTER ROLE authenticator RESET session_preload_libraries;

Ideal Fix: Either:

  • Include safeupdate.so/safeupdate.dylib in the bundle
  • Remove the session_preload_libraries=safeupdate from migrations for sandbox mode

4. pgsodium/vault getkey Script Paths

Status: Fixed with CLI workaround

Issue: The postgresql.conf.template has commented-out paths for encryption key scripts:

#pgsodium.getkey_script = '/path/to/pgsodium_getkey.sh'
#vault.getkey_script = '/path/to/pgsodium_getkey.sh'

Without these, postgres fails to start with pgsodium/vault enabled.

Current Workaround: Append explicit paths after copying template:

// sandbox.go:129-130
conf += fmt.Sprintf("\npgsodium.getkey_script = '%s'\n", pgsodiumScript)
conf += fmt.Sprintf("vault.getkey_script = '%s'\n", pgsodiumScript)

Ideal Fix: Use relative paths in the template, or document the expected configuration.


5. pg_hba.conf Authentication

Status: Fixed with CLI workaround

Issue: The bundled pg_hba.conf.template uses password authentication:

host    all    all    127.0.0.1/32    scram-sha-256

But initdb creates the superuser without a password, so connections fail:

FATAL: password authentication failed for user "supabase_admin"
DETAIL: User "supabase_admin" has no password assigned.

Current Workaround: Override with trust auth for localhost:

// sandbox.go:136-152
host    all    all    127.0.0.1/32    trust

Ideal Fix: Either:

  • Use trust in the template for local development
  • Document that passwords must be set after initdb
  • Provide a separate template for sandbox mode

6. Locale Path - Hardcoded Nix Path (Warning)

Status: Warning only - not blocking

Issue: The binary tries to use a Nix-specific locale binary:

sh: /nix/store/zkzf0q482kvjs7bb6467sgvbzwj27ggj-adv_cmds-235/bin/locale: No such file or directory
WARNING: no usable system locales were found

Impact: Warning messages during initdb, but doesn't break functionality.

Ideal Fix: Build without hardcoded Nix paths for locale detection.


Summary: Ideal Bundle Requirements

For the bundle to work without workarounds:

Issue Fix Required
Code signing Sign binaries during packaging
Timezone data Include tzdata OR build without --with-system-tzdata
safeupdate Include extension OR remove from migrations
pgsodium paths Use relative/templated paths in config
pg_hba.conf Use trust for local dev OR document setup
Locale paths Build without Nix-hardcoded paths

@jgoux
Copy link
Contributor

jgoux commented Feb 4, 2026

I asked Claude to compare what we're doing in the Docker image vs what's included in the artifact.

How the Docker Image Works (Reference)

The Supabase postgres Docker image (supabase/postgres) handles initialization differently and doesn't have these issues. Understanding this helps define what the bundle should include.

Docker Initialization Flow

  1. Container starts with POSTGRES_PASSWORD env var

    // internal/db/start/start.go:64
    "POSTGRES_PASSWORD=" + utils.Config.Db.Password,
  2. docker-entrypoint.sh runs (from official postgres image):

    • Checks if data directory exists
    • Runs initdb if needed
    • Creates postgres superuser WITH the password from POSTGRES_PASSWORD
    • Runs any init scripts in /docker-entrypoint-initdb.d/
    • Starts postgres
  3. Roles are created with hardcoded password 'postgres' in globals.sql:

    -- internal/utils/templates/globals.sql:19,35
    ALTER ROLE authenticator WITH ... PASSWORD 'postgres';
    ALTER ROLE supabase_admin WITH SUPERUSER ... PASSWORD 'postgres';
  4. schema.sql updates passwords to the actual configured password:

    -- internal/db/start/templates/schema.sql:8-14
    \set pgpass `echo "$PGPASSWORD"`
    ALTER USER postgres WITH PASSWORD :'pgpass';
    ALTER USER authenticator WITH PASSWORD :'pgpass';
    -- etc.

Why Docker Works and Bundle Doesn't

Aspect Docker Image Native Bundle
Init script docker-entrypoint.sh sets password during init No init script - initdb creates user without password
pg_hba.conf scram-sha-256 works because passwords are set scram-sha-256 fails - no passwords yet
Timezone data Bundled or proper system paths Hardcoded Nix paths that don't exist
Code signing Linux - not needed macOS - invalid signatures after extraction
Extensions All included (safeupdate, etc.) safeupdate missing

Recommended Bundle Changes

To make the bundle work like the Docker image without CLI workarounds:

1. Add an Initialization Script

Include a supabase-postgres-init script similar to docker-entrypoint.sh:

# share/supabase-cli/bin/supabase-postgres-init.sh
#!/bin/bash
set -e

PGDATA="${PGDATA:-./data}"
POSTGRES_USER="${POSTGRES_USER:-supabase_admin}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-postgres}"

# Get the directory where this script lives
SCRIPT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
PGBIN="$SCRIPT_DIR/bin"

# Initialize if needed
if [ ! -s "$PGDATA/PG_VERSION" ]; then
    "$PGBIN/initdb" -D "$PGDATA" -U "$POSTGRES_USER" --encoding=UTF8 --locale=C

    # Set password for the superuser immediately (using single-user mode)
    "$PGBIN/postgres" --single -D "$PGDATA" -j <<-EOSQL
        ALTER USER "$POSTGRES_USER" WITH PASSWORD '$POSTGRES_PASSWORD';
EOSQL

    # Copy config templates
    cp "$SCRIPT_DIR/share/supabase-cli/config/postgresql.conf.template" "$PGDATA/postgresql.conf"
    cp "$SCRIPT_DIR/share/supabase-cli/config/pg_hba.conf.template" "$PGDATA/pg_hba.conf"
fi

# Start postgres (or exec to replace shell)
exec "$PGBIN/postgres" -D "$PGDATA" "$@"

2. Bundle Timezone Data

Option A (Recommended): Build postgres WITHOUT --with-system-tzdata

  • This uses bundled tzdata in share/postgresql/timezone/
  • No hardcoded paths

Option B: Include tzdata in bundle

  • Add share/zoneinfo/ directory
  • Build with that relative path

3. Include safeupdate Extension

Add to bundle:

lib/safeupdate.so              # Linux
lib/safeupdate.dylib           # macOS
share/extension/safeupdate.control
share/extension/safeupdate--*.sql

4. Fix Config Templates

pg_hba.conf.template - Can use scram-sha-256 if init script sets password:

# pg_hba.conf - passwords are set by init script
local   all    all                     scram-sha-256
host    all    all    127.0.0.1/32     scram-sha-256
host    all    all    ::1/128          scram-sha-256

postgresql.conf.template - Use relative/discoverable paths:

# Use path relative to PGDATA or make it configurable
# pgsodium.getkey_script should be set by init script based on install location

5. Sign macOS Binaries During Build

Add to build process:

# Sign all binaries and dylibs before creating zip
find . -type f \( -perm +111 -o -name "*.dylib" \) -exec codesign -s "Developer ID" {} \;

Or use ad-hoc signing:

find . -type f \( -perm +111 -o -name "*.dylib" \) -exec codesign -f -s - {} \;

6. Build Without Nix-Specific Paths

Remove from configure:

  • --with-system-tzdata=/nix/store/...
  • Any locale paths pointing to /nix/store/

Ideal Bundle Structure

supabase-postgres-<version>-<os>-<arch>.zip
├── bin/
│   ├── postgres
│   ├── initdb
│   ├── psql
│   ├── pg_isready
│   └── ...
├── lib/
│   ├── *.so / *.dylib           # All shared libraries
│   ├── safeupdate.so            # MISSING: include this
│   └── ...
├── share/
│   ├── postgresql/
│   │   ├── timezone/            # NEEDED: bundled tzdata
│   │   └── extension/
│   │       └── safeupdate*      # MISSING: include this
│   └── supabase-cli/
│       ├── bin/
│       │   └── supabase-postgres-init.sh   # NEW: init script
│       ├── config/
│       │   ├── postgresql.conf.template
│       │   ├── pg_hba.conf.template
│       │   └── pgsodium_getkey.sh
│       └── migrations/
│           └── migrate.sh

Summary of Build Changes Required

Issue Build Change
Timezone data Remove --with-system-tzdata=/nix/store/...
Locale paths Remove Nix-specific locale paths
Code signing (macOS) Add codesign step to build
safeupdate Add to extension build list
Config paths Use relative paths or make configurable
Init script Add supabase-postgres-init.sh wrapper

With these changes, the CLI would only need to:

  1. Extract the zip
  2. Run supabase-postgres-init.sh with appropriate env vars
  3. Run migrations

No workarounds needed.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch 2 times, most recently from 814d460 to 3065354 Compare February 4, 2026 17:42
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@nix/packages/postgres-portable.nix`:
- Around line 223-242: The timezone-data verification block that checks for
timezone/ and timezonesets/ (the commented shell snippet in the file) must be
either re-enabled or documented: if the removal of --with-system-tzdata in
postgres.nix actually bundles tzdata, uncomment and restore the verification
block to assert $out/share/postgresql/timezone or $out/share/timezonesets (and
the alternate $out/share paths) and fail early on missing data; otherwise add a
clear comment above that block referencing the change to --with-system-tzdata in
postgres.nix and why the runtime check is intentionally disabled so reviewers
know this is deliberate.
🧹 Nitpick comments (3)
nix/checks.nix (1)

780-780: Duplicate section numbering: two "Section 8" headers.

Line 780 has "Section 8: Verifying wrapper scripts" and line 838 has "Section 8: Checking for hardcoded /nix/store paths". The second should be "Section 10".

📝 Proposed fix
-                echo "=== Section 8: Checking for hardcoded /nix/store paths in binaries ==="
+                echo "=== Section 10: Checking for hardcoded /nix/store paths in binaries ==="

Also applies to: 838-838

nix/packages/cli-config/postgresql.conf.template (1)

35-43: Consider documenting portable relative path pattern.

The commented getkey script paths require users to manually configure after copying. Per PR feedback, consider documenting the portable relative path pattern or providing a helper to auto-configure based on bundle location.

💡 Suggested documentation improvement
 # pgsodium and vault configuration
 # Set the path to your pgsodium_getkey.sh script
 # Default location when using portable bundle: share/supabase-cli/config/pgsodium_getkey.sh
+# For portable bundles, use paths relative to PGDATA or absolute paths to the bundle:
+#   Example: pgsodium.getkey_script = '/path/to/bundle/share/supabase-cli/config/pgsodium_getkey.sh'
 `#pgsodium.getkey_script` = '/path/to/pgsodium_getkey.sh'
 `#vault.getkey_script` = '/path/to/pgsodium_getkey.sh'
.github/workflows/cli-release.yml (1)

188-194: Extension verification incomplete - missing CLI extensions.

The check verifies pg_graphql, pgcrypto, uuid-ossp, supabase_vault, but doesn't verify pgsodium, pg_net, pg_cron, or supautils which are in the CLI extension set.

🔧 Proposed fix
           # Check for required extensions
-          for ext in pg_graphql pgcrypto uuid-ossp supabase_vault; do
+          for ext in pg_graphql pgcrypto uuid-ossp supabase_vault pgsodium pg_net pg_cron supautils; do
             if ! grep -q "$ext" extensions.log; then
               echo "Required extension $ext not found"
               exit 1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@nix/packages/postgres-portable.nix`:
- Around line 334-373: The macOS Mach-O patching loop (the for bin in
$out/bin/.*-wrapped and for lib in $out/lib/*.dylib* blocks using
otool/install_name_tool) leaves binaries unsigned causing "Code Signature
Invalid" crashes; after completing install_name_tool changes add an ad-hoc
codesign step to re-sign each patched executable and dylib (e.g., run codesign
-f -s - on the same $bin and $lib files), gated by the existing stdenv.isDarwin
conditional and keeping errors non-fatal (redirect or || true) so packaging
continues.
- Around line 29-48: The migrations bundle references the safeupdate extension
but the CLI build excludes it via cliExtensions causing runtime failures; either
update the migration file 20220118070449_enable-safeupdate-postgrest.sql to wrap
the session_preload_libraries = 'safeupdate' change in a guard that checks for
the extension (similar to the conditional used in
20220224211803_fix-postgrest-supautils.sql), or add the pg-safeupdate.nix entry
into cliExtensions so psql_17_cli provides the safeupdate extension; modify the
migration SQL to use the same EXISTS/DO/IF logic as the supautils migration or
update postgres.nix to include pg-safeupdate.nix in cliExtensions accordingly.
🧹 Nitpick comments (2)
nix/packages/postgres-portable.nix (2)

79-91: Consider excluding additional system libraries.

The exclusion list covers core glibc libraries but misses some commonly system-provided libraries that could cause ABI mismatches if bundled:

  • libgcc_s.so* - GCC runtime (tied to system compiler)
  • libstdc++.so* - C++ standard library (if any C++ code is involved)
  • libnsl.so*, libnss_*.so* - Name service libraries

Bundling these from Nix while the host uses different versions can cause subtle runtime issues.

💡 Suggested addition
       case "$libname" in
         libc.so*|libc-*.so*|ld-linux*.so*|libdl.so*|libpthread.so*|libm.so*|libresolv.so*|librt.so*)
           return 0  # Exclude
           ;;
+        libgcc_s.so*|libstdc++.so*|libnsl.so*|libnss_*.so*)
+          return 0  # Exclude - system ABI-coupled libraries
+          ;;
         *)
           return 1  # Include
           ;;

163-180: Silent failures when copying dependencies may produce incomplete bundles.

The cp "$dep" $out/lib/ 2>/dev/null || true pattern silently ignores copy failures. If a required dependency fails to copy (e.g., permission issues, disk space), the bundle will be incomplete but the build succeeds.

Consider at minimum logging when a copy fails, or tracking failures and reporting at the end.

💡 Suggested improvement
               if ! should_exclude_library "$libname"; then
-                cp "$dep" $out/lib/ 2>/dev/null || true
+                if ! cp "$dep" $out/lib/ 2>/dev/null; then
+                  echo "Warning: Failed to copy dependency $dep"
+                fi
               else

Comment on lines +334 to +338
+ lib.optionalString stdenv.isDarwin ''
# On macOS, patch binaries to use relative library paths
# This makes the bundle portable across macOS systems for Supabase CLI
for bin in $out/bin/.*-wrapped; do
if [ -f "$bin" ] && file "$bin" | grep -q "Mach-O"; then
# Get all dylib dependencies from Nix store
otool -L "$bin" | grep /nix/store | awk '{print $1}' | while read dep; do
libname=$(basename "$dep")
# Check if we have this library in our lib directory
if [ -f "$out/lib/$libname" ]; then
echo "Patching $bin: $dep -> @rpath/$libname"
install_name_tool -change "$dep" "@rpath/$libname" "$bin" 2>/dev/null || true
fi
done
# Add @rpath to look in @executable_path/../lib
install_name_tool -add_rpath "@executable_path/../lib" "$bin" 2>/dev/null || true
fi
done

# Patch dylibs to use @rpath for their dependencies
for lib in $out/lib/*.dylib*; do
if [ -f "$lib" ] && file "$lib" | grep -q "Mach-O"; then
# First, fix the library's own ID to use @rpath
libname=$(basename "$lib")
install_name_tool -id "@rpath/$libname" "$lib" 2>/dev/null || true

# Add @rpath to the library itself so it can find other libraries
install_name_tool -add_rpath "@loader_path" "$lib" 2>/dev/null || true

# Then fix references to other libraries
otool -L "$lib" | grep /nix/store | awk '{print $1}' | while read dep; do
deplibname=$(basename "$dep")
if [ -f "$out/lib/$deplibname" ]; then
echo "Patching $lib: $dep -> @rpath/$deplibname"
install_name_tool -change "$dep" "@rpath/$deplibname" "$lib" 2>/dev/null || true
fi
done
fi
done
'';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing macOS code signing causes "Code Signature Invalid" crashes.

Per reviewer feedback, extracted macOS binaries crash with "Code Signature Invalid" after the Mach-O patching. The current workaround requires users to manually re-sign with codesign -f -s -.

Consider adding ad-hoc signing after patching:

💡 Suggested addition
       done
+
+      # Ad-hoc sign all patched binaries and dylibs to fix code signature invalidation
+      echo "Ad-hoc signing binaries and libraries for macOS..."
+      for bin in $out/bin/.*-wrapped; do
+        if [ -f "$bin" ]; then
+          codesign -f -s - "$bin" 2>/dev/null || echo "Warning: Failed to sign $bin"
+        fi
+      done
+      for lib in $out/lib/*.dylib*; do
+        if [ -f "$lib" ]; then
+          codesign -f -s - "$lib" 2>/dev/null || echo "Warning: Failed to sign $lib"
+        fi
+      done
     '';
🤖 Prompt for AI Agents
In `@nix/packages/postgres-portable.nix` around lines 334 - 373, The macOS Mach-O
patching loop (the for bin in $out/bin/.*-wrapped and for lib in
$out/lib/*.dylib* blocks using otool/install_name_tool) leaves binaries unsigned
causing "Code Signature Invalid" crashes; after completing install_name_tool
changes add an ad-hoc codesign step to re-sign each patched executable and dylib
(e.g., run codesign -f -s - on the same $bin and $lib files), gated by the
existing stdenv.isDarwin conditional and keeping errors non-fatal (redirect or
|| true) so packaging continues.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch 3 times, most recently from 9bc3af3 to 345b8ff Compare February 4, 2026 20:24
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@nix/packages/postgres.nix`:
- Around line 194-201: The current assignment to postFixup is replacing upstream
fixup steps; instead, preserve upstream behavior and only remove the initdb
wrapper on Linux by appending a conditional snippet to the existing postFixup
(e.g., set postFixup = (old: old + lib.optionalString
pkgs.stdenv.hostPlatform.isLinux '' ... '') so you only inject the removal of
the initdb wrapper and do not discard other upstream fixups), reference
postFixup, lib.optionalString, pkgs.stdenv.hostPlatform.isLinux and the initdb
wrapper; if you truly intend to disable postFixup entirely for the CLI variant,
add an explicit comment documenting that decision.

Comment on lines 194 to 273
# CRITICAL FIX FOR LINUX: Override postFixup to remove initdb wrapper
# The upstream nixpkgs adds a wrapper on Linux that embeds glibc.bin path
# We remove this wrapper because 'locale' should be found from system PATH
postFixup = lib.optionalString pkgs.stdenv.hostPlatform.isLinux ''
# Do NOT wrap initdb with glibc.bin path
# The system's locale command will be used from PATH
# This is acceptable for portable CLI distribution
'';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full context around the lines in question
head -210 nix/packages/postgres.nix | tail -40

Repository: supabase/postgres

Length of output: 2545


🏁 Script executed:

# Check the structure of the entire postgres.nix file to understand the override pattern
wc -l nix/packages/postgres.nix

Repository: supabase/postgres

Length of output: 90


🏁 Script executed:

# Look for the override block containing this code
grep -n "override\|postFixup" nix/packages/postgres.nix | head -20

Repository: supabase/postgres

Length of output: 468


🏁 Script executed:

# Check the basePostgresql definition to see its postFixup
grep -n "basePostgresql" nix/packages/postgres.nix | head -10

Repository: supabase/postgres

Length of output: 204


🏁 Script executed:

# Look for any upstream postFixup references or check what basePostgresql pulls from
grep -n "inherit\|postgresql\|pkgs\." nix/packages/postgres.nix | head -30

Repository: supabase/postgres

Length of output: 1701


🏁 Script executed:

# Search for where basePostgresql is defined
grep -B 5 "basePostgresql\s*=" nix/packages/postgres.nix

Repository: supabase/postgres

Length of output: 195


🏁 Script executed:

# Let's understand what upstream nixpkgs postgres does with postFixup
# Search for what basePostgresql (pkgs.postgresql_*) might contain
# Check if there are any references to postFixup behavior in comments or documentation

grep -n "postFixup\|preConfigure\|previousAttrs" nix/packages/postgres.nix

Repository: supabase/postgres

Length of output: 484


🏁 Script executed:

# Let's check the git history or similar patterns to see if postFixup was intentionally appended before
grep -B 3 -A 3 "previousAttrs\." nix/packages/postgres.nix

Repository: supabase/postgres

Length of output: 1484


🏁 Script executed:

# Let's check if there's any actual content that should be in postFixup
# The block appears empty - let's verify this is intentional
sed -n '194,203p' nix/packages/postgres.nix

Repository: supabase/postgres

Length of output: 745


🏁 Script executed:

# Let's see if there are any other patterns or comments explaining the postFixup intent
sed -n '190,210p' nix/packages/postgres.nix

Repository: supabase/postgres

Length of output: 1443


Clarify intent: does the CLI variant need postFixup disabled entirely, or just the initdb wrapper removed?

The current assignment postFixup = lib.optionalString pkgs.stdenv.hostPlatform.isLinux ''...'' replaces the entire upstream postFixup with an empty string on all platforms, discarding any upstream fixup steps that may be critical for portability. This differs from how preConfigure is handled (appended, not replaced).

If the goal is simply to avoid the initdb wrapper while preserving other fixups, use the safer pattern:

Recommended fix
-                  postFixup = lib.optionalString pkgs.stdenv.hostPlatform.isLinux ''
+                  postFixup = (previousAttrs.postFixup or "") + lib.optionalString pkgs.stdenv.hostPlatform.isLinux ''
                     # Do NOT wrap initdb with glibc.bin path
                     # The system's locale command will be used from PATH
                     # This is acceptable for portable CLI distribution
                   '';

If disabling postFixup entirely for the CLI variant is intentional and safe, document this explicitly.

🤖 Prompt for AI Agents
In `@nix/packages/postgres.nix` around lines 194 - 201, The current assignment to
postFixup is replacing upstream fixup steps; instead, preserve upstream behavior
and only remove the initdb wrapper on Linux by appending a conditional snippet
to the existing postFixup (e.g., set postFixup = (old: old + lib.optionalString
pkgs.stdenv.hostPlatform.isLinux '' ... '') so you only inject the removal of
the initdb wrapper and do not discard other upstream fixups), reference
postFixup, lib.optionalString, pkgs.stdenv.hostPlatform.isLinux and the initdb
wrapper; if you truly intend to disable postFixup entirely for the CLI variant,
add an explicit comment documenting that decision.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch 5 times, most recently from 3b7417d to d9cb921 Compare February 4, 2026 21:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@nix/checks.nix`:
- Around line 215-256: The cliSkipTests array in nix/checks.nix includes "plv8"
but there is no corresponding test file; remove the "plv8" entry from the
cliSkipTests list (or alternatively add a matching test file under
nix/tests/sql/ if the test is required) so filteredTestList and the test
basename mapping no longer reference a non-existent test; update the
cliSkipTests array (symbol: cliSkipTests) and re-run the filter logic (symbol:
filteredTestList) to ensure no missing basenames remain.

In `@nix/packages/cli-config/pg_hba.conf.template`:
- Around line 5-9: The pg_hba.conf.template currently requires scram-sha-256 for
loopback TCP ("host all all 127.0.0.1/32 scram-sha-256" and "host all all
::1/128 scram-sha-256"), which will reject initdb-created passwordless superuser
connections; update the CLI-friendly template to use "trust" for those loopback
host rules or alternatively make the init wrapper set a password before enabling
SCRAM—modify the entries in pg_hba.conf.template for 127.0.0.1/32 and ::1/128 to
"trust" (or implement password-setting in the init script that runs before
switching these entries to scram-sha-256).

Comment on lines +215 to +256
# Tests to skip for CLI variants (require extensions not in CLI)
cliSkipTests = [
# Extension-specific tests
"evtrigs"
"http"
"hypopg"
"index_advisor"
"pg_hashids"
"pg_jsonschema"
"pg_partman"
"pg_repack"
"pg_tle"
"pgtap"
"pgmq"
"pgroonga"
"pgrouting"
"plpgsql-check"
"plv8"
"postgis"
"postgres_fdw"
# Tests that depend on extensions not in CLI
"security" # depends on various extensions
"extensions_schema" # tests extension loading
"roles" # includes roles/schemas from extensions not in CLI (pgtle, pgmq, repack, topology)
# Version-specific extension tests
"z_17_ext_interface"
"z_17_pg_stat_monitor"
"z_17_pgvector"
"z_17_rum"
"z_17_roles" # version-specific roles test, includes pgtle_admin
];

# Convert filtered tests to a sorted list of basenames (without extension)
testList = pkgs.lib.mapAttrsToList (
name: _: builtins.substring 0 (pkgs.lib.stringLength name - 4) name
) filteredSqlTests;
sortedTestList = builtins.sort (a: b: a < b) testList;

# Filter out CLI-incompatible tests if this is a CLI variant
filteredTestList =
if isCliVariant then
builtins.filter (test: !(builtins.elem test cliSkipTests)) testList
else
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare cliSkipTests entries against nix/tests/sql/*.sql basenames.
python - <<'PY'
import re, pathlib
text = pathlib.Path("nix/checks.nix").read_text()
m = re.search(r"cliSkipTests\s*=\s*\[(.*?)\];", text, re.S)
skip = re.findall(r'"([^"]+)"', m.group(1)) if m else []
tests = [p.stem for p in pathlib.Path("nix/tests/sql").glob("*.sql")]
missing = [s for s in skip if s not in tests]
extra = [t for t in tests if t in skip]
print("Skip entries without matching test file:", missing)
print("Skip entries that match:", extra)
PY

Repository: supabase/postgres

Length of output: 467


Remove plv8 from cliSkipTests—it has no corresponding test file.
Verification shows plv8 is listed in the skip list but lacks a matching test file in nix/tests/sql/. Most other entries (including plpgsql-check) correctly match their test basenames. Remove plv8 or create the missing test file.

🤖 Prompt for AI Agents
In `@nix/checks.nix` around lines 215 - 256, The cliSkipTests array in
nix/checks.nix includes "plv8" but there is no corresponding test file; remove
the "plv8" entry from the cliSkipTests list (or alternatively add a matching
test file under nix/tests/sql/ if the test is required) so filteredTestList and
the test basename mapping no longer reference a non-existent test; update the
cliSkipTests array (symbol: cliSkipTests) and re-run the filter logic (symbol:
filteredTestList) to ensure no missing basenames remain.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch 3 times, most recently from 00bdb0f to d3de8d8 Compare February 4, 2026 22:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@nix/packages/postgres-portable.nix`:
- Around line 50-61: Uncomment and set the getkey_script paths in the
postgresql.conf.template so the portable bundle points to the installed script
path (installed by configBundle) — update the pgsodium.getkey_script and
vault.getkey_script entries in postgresql.conf.template to reference the bundle
location (share/supabase-cli/config/pgsodium_getkey.sh) or implement a startup
hook in the CLI to rewrite those config keys at launch; ensure references align
with the install path created by configBundle (the directory created in
installPhase) and document the behavior so pgsodium and vault can locate the
getkey script when the portable bundle is used.

Comment on lines +50 to +62
configBundle = stdenv.mkDerivation {
name = "cli-config-bundle";
src = configDir;
dontPatchShebangs = true;
installPhase = ''
mkdir -p $out/share/supabase-cli/config
cp postgresql.conf.template $out/share/supabase-cli/config/
cp pg_hba.conf.template $out/share/supabase-cli/config/
cp pg_ident.conf.template $out/share/supabase-cli/config/
cp pgsodium_getkey.sh $out/share/supabase-cli/config/
chmod +x $out/share/supabase-cli/config/pgsodium_getkey.sh
'';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "\.(template|sh)$" nix/packages/

Repository: supabase/postgres

Length of output: 244


🏁 Script executed:

echo "=== pg_hba.conf.template ===" && cat nix/packages/cli-config/pg_hba.conf.template && \
echo -e "\n=== pgsodium_getkey.sh ===" && cat nix/packages/cli-config/pgsodium_getkey.sh && \
echo -e "\n=== postgresql.conf.template ===" && head -50 nix/packages/cli-config/postgresql.conf.template

Repository: supabase/postgres

Length of output: 2208


🏁 Script executed:

cat -n nix/packages/postgres-portable.nix | head -80

Repository: supabase/postgres

Length of output: 3062


🏁 Script executed:

rg -i "getkey_script|pgsodium.*path|postgresql\.conf" --type sh --type rs --type ts --type go -A 3 -B 1 | head -100

Repository: supabase/postgres

Length of output: 87


🏁 Script executed:

rg -i "getkey_script|pgsodium.*path" -A 2 -B 1 | head -80

Repository: supabase/postgres

Length of output: 5047


🏁 Script executed:

grep -r "run-server\|launcher\|wrapper" nix/packages/postgres-portable.nix || echo "Not found in postgres-portable.nix"

Repository: supabase/postgres

Length of output: 345


🏁 Script executed:

rg "configBundle|postgresql\.conf\.template" nix/packages/ -A 5 -B 5 | head -60

Repository: supabase/postgres

Length of output: 2061


🏁 Script executed:

cd nix/packages && grep -A 30 "buildPhase\|installPhase" postgres-portable.nix | grep -E "run-server|launcher|bin/" | head -20

Repository: supabase/postgres

Length of output: 261


🏁 Script executed:

# Check what's actually included in the final output
grep -A 50 "stdenv.mkDerivation {" nix/packages/postgres-portable.nix | grep -E "mkdir|cp|bin/|share/" | head -30

Repository: supabase/postgres

Length of output: 1025


🏁 Script executed:

# Look for documentation or CLI integration
find . -name "README*" -o -name "*.md" | head -10

Repository: supabase/postgres

Length of output: 362


🏁 Script executed:

# Search for references to share/supabase-cli/config
rg "share/supabase-cli/config|cli-config" --type md --type txt -A 2 -B 1

Repository: supabase/postgres

Length of output: 43


🏁 Script executed:

cat README.md | grep -A 20 -i "portable\|cli\|config" | head -50

Repository: supabase/postgres

Length of output: 3259


🏁 Script executed:

# Search for where CLI might process these templates
rg "share/supabase-cli" -l

Repository: supabase/postgres

Length of output: 144


🏁 Script executed:

# Look for any comments or issues about template configuration in the codebase
rg "placeholder|path.*template|getkey.*path" --type nix -B 2 -A 2 | head -80

Repository: supabase/postgres

Length of output: 379


🏁 Script executed:

# Check if there's a mechanism in the nix package to handle path substitution
tail -100 nix/packages/postgres-portable.nix

Repository: supabase/postgres

Length of output: 4291


Uncomment and configure getkey_script paths in postgresql.conf.template for portable bundle usage.

The template has pgsodium.getkey_script and vault.getkey_script commented out with a placeholder path. Since the portable bundle installs the script to share/supabase-cli/config/pgsodium_getkey.sh, these lines should be uncommented and configured to reference that location, or a documented mechanism must exist for the CLI to set these paths at startup. Without this configuration, the pgsodium and vault extensions won't function. The pg_hba.conf configuration (trust for local, scram-sha-256 for network) is appropriate for passwordless init.

🤖 Prompt for AI Agents
In `@nix/packages/postgres-portable.nix` around lines 50 - 61, Uncomment and set
the getkey_script paths in the postgresql.conf.template so the portable bundle
points to the installed script path (installed by configBundle) — update the
pgsodium.getkey_script and vault.getkey_script entries in
postgresql.conf.template to reference the bundle location
(share/supabase-cli/config/pgsodium_getkey.sh) or implement a startup hook in
the CLI to rewrite those config keys at launch; ensure references align with the
install path created by configBundle (the directory created in installPhase) and
document the behavior so pgsodium and vault can locate the getkey script when
the portable bundle is used.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch 2 times, most recently from 7b41b7c to fc53a9e Compare February 5, 2026 01:26
@LGUG2Z
Copy link
Author

LGUG2Z commented Feb 5, 2026

@jgoux

I've updated the PR to

  • Comprehensively strip out hardcoded paths at compile time and added the integration test we discussed on Slack
  • Added the safeupdate ext
  • Use trust in the hba conf for now until we decide how to move ahead with the init

I have not made any changes to the template - I think it's normal to update a config template at runtime

Adhoc codesigning isn't working and is hard to debug in CI, I think it's ok to continue with client-side codesigning for macOS for now since that is what homebrew kegs do when installing

Let's discuss the init with others on Slack and then we can update this PR with whatever is decided

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch from fc53a9e to a35290e Compare February 5, 2026 01:41
@LGUG2Z
Copy link
Author

LGUG2Z commented Feb 5, 2026

@CodeRabbit pause

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

✅ Actions performed

Reviews paused.

@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch from a35290e to 47c345c Compare February 5, 2026 01:46
…ibution

This commit implements portable, self-contained PostgreSQL binaries for
the Supabase CLI across macOS (ARM), Linux (x64), and Linux (ARM64),
along with automated CI/CD workflows for building and releasing these
artifacts.

The Supabase CLI needs to ship PostgreSQL binaries that work on user
machines without requiring Nix or other system dependencies. This means
extracting the actual binaries from Nix's wrapper scripts, bundling all
necessary shared libraries, and patching them to use relative paths
instead of hardcoded Nix store paths.

A `variant` parameter was added to the postgres build pipeline to
distinguish between "full" (all extensions) and "cli" (minimal
extensions for Supabase CLI). The `cliExtensions` list contains 6
extensions required for running Supabase migrations: supautils,
pg_graphql, pgsodium, supabase_vault, pg_net, and pg_cron. Built-in
extensions (uuid-ossp, pgcrypto, pg_stat_statements) are included
automatically with PostgreSQL. `makeOurPostgresPkgs`/`makePostgresBin`
were modified to accept this parameter. A new `psql_17_cli` package is
created using `variant = "cli"`, while the full extension set is
preserved for base packages (`psql_15`, `psql_17`, `psql_orioledb-17`).

The portable CLI variant (`psql_17_cli_portable`) includes 6 extensions
for migration support while maintaining a significantly smaller size
than the full build. The implementation in
`nix/packages/postgres-portable.nix` extracts binaries from
`psql_17_cli` using a `resolve_binary()` function that follows wrapper
layers to find the actual ELF/Mach-O binaries behind Nix's environment
setup scripts.

All Nix-provided libraries (ICU, readline, zlib, etc.) are bundled while
excluding system libraries (`libc`, `libpthread`, `libm`, `glibc`,
`libdl`) that must come from the host. This distinction is critical:
Linux bundles must exclude glibc due to kernel ABI dependencies, while
macOS can include more libs due to its different linking model.
Dependency resolution runs multiple passes to catch transitive deps
(e.g., ICU → charset → etc.).

Platform-specific patching is applied: Linux binaries use the system
interpreter (`/lib64/ld-linux-*.so.2`) and `$ORIGIN`-based RPATHs, while
macOS binaries use `@rpath` with `@executable_path`. Wrapper scripts set
`LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS) to find bundled
libraries.

The bundle includes PostgreSQL config templates (`postgresql.conf`,
`pg_hba.conf`, `pg_ident.conf`) tailored for CLI usage with minimal
local dev settings, plus the complete Supabase migration script
(`migrate.sh`) with all init-scripts and migration SQL files (55 files,
236KB).

To achieve true portability, a `portable` parameter was added to the
PostgreSQL build in `nix/postgresql/generic.nix`. When `portable =
true`, three critical hardcoded paths are excluded from the build:

1. `--with-system-tzdata` is removed from configure flags, allowing
   PostgreSQL to use bundled timezone data from the `share/` directory
   instead of a hardcoded `/nix/store/.../tzdata` path;

2. the `locale-binary-path.patch` is excluded, so PostgreSQL calls
   `locale -a` from system PATH rather than using an absolute path to
   glibc's locale command;

3. the `postFixup` initdb wrapper is disabled to avoid hardcoding glibc
   paths. The `portable` parameter defaults to `false` in
   `nix/postgresql/default.nix` but is overridden to `true` for the CLI
   variant in `nix/packages/postgres.nix` using `.override { portable =
   true; }`.

This ensures standard PostgreSQL builds remain unchanged while the CLI
variant produces truly portable binaries without any `/nix/store`
references.

A GitHub Actions workflow builds portable binaries across all three
platforms using a matrix strategy. Each build runs automated portability
checks that verify no `/nix/store` references remain, validate RPATH
configuration, confirm transitive dependencies are bundled, ensure
system libraries are NOT bundled, and check wrapper scripts contain
proper library path setup.

Post-build testing validates binaries work without Nix (`postgres
--version`, `psql --version`), that Supabase migrations can run
successfully, and that tzdata is available.

On tagged releases (`v*-cli`), the workflow creates GitHub releases with
tarball artifacts and checksums.

The test infrastructure needed significant changes to support variants
with different extension sets. An `isCliVariant` parameter was added to
`makeCheckHarness`, and the hardcoded `shared_preload_libraries` list in
`postgresql.conf.in` was replaced with a `@PRELOAD_LIBRARIES@`
placeholder. A `generatePreloadLibs` script now parses `receipt.json` at
test time and dynamically builds the preload list based on available
extensions, removing the previous timescaledb removal hack for OrioleDB.
@LGUG2Z LGUG2Z force-pushed the feature/sb-cli-binaries branch from 47c345c to bd4b787 Compare February 5, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants