Skip to content

feat(compose): deployment-mode toggles for Supabase + storage#3

Merged
Lef-F merged 14 commits into
mainfrom
lef/deployment-modes
May 12, 2026
Merged

feat(compose): deployment-mode toggles for Supabase + storage#3
Lef-F merged 14 commits into
mainfrom
lef/deployment-modes

Conversation

@Lef-F
Copy link
Copy Markdown
Owner

@Lef-F Lef-F commented May 12, 2026

What

Adds two env vars in .env that select, per axis, whether each backplane is bundled (containers we ship) or external (cloud / BYO):

  • MIKE_SUPABASE_MODE ∈ {bundled-full, bundled-byo-db, external}
  • MIKE_STORAGE_MODE ∈ {bundled, external}

3 × 2 = 6 combinations. Defaults (bundled-full + bundled) match the current stack exactly, so existing installs upgrade with no config changes.

New ./mike CLI wrapper reads the modes, validates them, and dispatches to docker compose with the right --profile flags. ./mike up, ./mike down, ./mike build, ./mike logs, ./mike migrate, plus ./mike --print-profiles for debugging.

Why

The original compose stack required running every component locally. Many self-hosters want to combine self-host compute with managed Postgres (Neon, RDS), Hosted Supabase, or managed S3 (R2, AWS, MinIO). The single-toggle model handles all six combos from one compose file.

The toggle names + EXTERNAL_* env conventions are designed to translate 1:1 to a future values.yaml, so the same mental model carries to Helm/k3s when that lands (separate PR — see "Not in this PR" below).

How

  • Compose profiles (docker-compose.yml): five services gain profiles: gates — postgres (db-bundled), gotrue + postgrest (supabase-shim), garage + init-garage (storage-bundled). Backend env and frontend build args use ${EXTERNAL_*:-fallback} defaulted lookups so one file covers every combo. depends_on edges that would have auto-deselected always-on services through profile gating were removed; their script-level waits cover ordering.
  • ./mike CLI wrapper (mike): bash, repo-root, executable. POSIX-shell-style awk parses .env without sourcing (no arbitrary shell execution). Validates modes, computes --profile flags, dispatches. Bash 3.2-safe empty-array splice for the all-external case on stock macOS.
  • Mode-aware migrations (docker/init-db.sh): accepts PG_URL (hosted Supabase / BYO Postgres) with fallback to PGHOST/PGUSER/PGPASSWORD/PGDATABASE. Bundled-fallback branch passes -h/-U/-d as args and lets psql read PGPASSWORD from env, so the password never lands in URL form and never leaks to docker compose logs. for-loop over [0-9][0-9][0-9]_*.sql glob with no-match guard.
  • Mode-aware backend entrypoint (docker/backend-entrypoint.sh): skips sourcing /run/secrets/garage.env when R2_ACCESS_KEY_ID is already in env (external storage mode); waits up to 60s in bundled mode for init-garage to write the file.
  • Mode-aware secrets (scripts/generate-secrets.sh): generates only the secrets the chosen modes need (no JWT/SUPABASE keys in external Supabase mode; no GARAGE_* tokens in external storage mode). Emits WARN: … lines on stderr for missing operator-supplied EXTERNAL_* / R2_* values, including the bundled-byo-db requirements for EXTERNAL_POSTGRES_AUTHENTICATOR_URL and EXTERNAL_SUPABASE_GOTRUE_PG_URL.
  • Env-driven S3 client (backend/src/lib/storage.ts): extracted buildS3Config() from getClient(). Adds R2_REGION (default auto) and R2_FORCE_PATH_STYLE (default false) reads, unlocking AWS S3 (real region for SigV4) and MinIO (path-style). 4-line behavioral change, defaults preserve Garage exactly.
  • .env.example rewritten around the toggle model — each section labels which mode(s) need it, EXTERNAL_* groups are visible and documented.
  • README gains a ### Deployment modes subsection with the six-combo matrix, switching procedure (including the ./mike build mike-frontend rebuild when crossing into/out of external Supabase mode), and a hosted-Supabase migration note.

Test plan / verification

  • Backend unit tests: 21/21 (4 new in backend/test/storage.test.ts + 17 existing). Each test wrapped in try { … } finally { restoreEnv(); } so an assertion throw doesn't leak env to the next test.
  • Wrapper shell test (test/mike.test.sh): 10/10 — 6 valid mode combos + 2 invalid-mode rejections + 2 cases locking the read_var regex (# is only a comment when preceded by whitespace).
  • Shellcheck across 6 touched scripts: 0 errors (one expected SC1091 info for the runtime-generated /run/secrets/garage.env).
  • docker compose config --services validated for all 6 profile combinations — each produces exactly the expected service set.
  • Live smoke test (scripts/smoke-test.sh) against full Docker stack on a laptop: stack came up, Caddy responded on :80, /backend/health returned {"ok":true}, clean teardown via ./mike down --remove-orphans. Default mode (bundled-full + bundled) only; other modes need manual verification with real external services.

Not in this PR

  • Helm chart for k3s / k8s. Separate follow-up PR. The toggle model, EXTERNAL_* env conventions, and init-db.sh's mode-awareness are all designed so the same shapes translate to values.yaml and templated manifests without changing this PR's surface.

Commits

14 commits, conventional commit style. Several fix(...) commits land mid-chain — they're review-loop corrections caught by spec-compliance + code-quality review during development, kept as separate commits so the PR history is auditable instead of squashed into a single mega-feat commit.

Lef-F added 14 commits May 12, 2026 10:26
Extract buildS3Config() with env-driven region (R2_REGION, default 'auto')
and forcePathStyle (R2_FORCE_PATH_STYLE=true|*, default false). Defaults
preserve current Garage behavior; new envs unlock AWS S3 (real region)
and MinIO (path-style).
Reorganize .env.example around MIKE_SUPABASE_MODE and MIKE_STORAGE_MODE.
Each section labels which mode(s) need it, and external-mode secrets get
their own variables (EXTERNAL_SUPABASE_*, EXTERNAL_POSTGRES_URL) so they
don't collide with the bundled-mode generated ones.
Add 'profiles:' to postgres, gotrue, postgrest, garage, init-garage.
Use defaulted EXTERNAL_*:- lookups in the backend env block and frontend
build args so the same compose file covers all six MIKE_SUPABASE_MODE x
MIKE_STORAGE_MODE combinations.

Remove depends_on edges that would auto-deselect always-on services when
their profile-gated targets are inactive (init-db, mike-backend, caddy);
service scripts already handle the waits.
Move &search_path=auth inside the default expression so operators providing
EXTERNAL_POSTGRES_URL without a query string don't get a broken URL with
'&' instead of '?' as the first separator. Introduce a dedicated
EXTERNAL_SUPABASE_GOTRUE_PG_URL var (matches the existing
EXTERNAL_POSTGRES_AUTHENTICATOR_URL pattern) so operators can override
GoTrue's connection string without affecting PostgREST/init-db. Also
declare EXTERNAL_POSTGRES_AUTHENTICATOR_URL in .env.example — it was
referenced in compose but missing from the operator-facing template.
init-db.sh: accept PG_URL connection string (preferred), fall back to
PGHOST/PGUSER/PGPASSWORD/PGDATABASE. Drops the GoTrue-healthcheck wait
in favor of the auth.users-table poll, which works for hosted Supabase
where GoTrue isn't ours.

backend-entrypoint.sh: skip /run/secrets/garage.env source when
R2_ACCESS_KEY_ID is already in env (external storage). Wait up to 60s
in bundled mode for init-garage to write the file.
Bundled-fallback branch now passes -h/-U/-d flags to psql/pg_isready
and lets psql read PGPASSWORD silently from env, so a libpq error
message can't echo the password into docker logs. PG_URL branch
unchanged (operator chose to put creds in URL themselves).

Replace 'ls /migrations/... | sort | while' with a 'for' loop over a
literal glob, with case-based handling for the no-match scenario.
Eliminates SC2012, surfaces empty/missing migrations dir explicitly
via the 'applied N incremental migrations' summary line.
Reads MIKE_SUPABASE_MODE and MIKE_STORAGE_MODE from .env, validates them,
maps to docker compose --profile flags, dispatches. Adds shell tests for
all six valid combos plus invalid-mode rejection.
1. Use ${PROFILES[@]+"${PROFILES[@]}"} so the all-external mode
   (empty PROFILES array) doesn't trip 'unbound variable' on Bash 3.2
   (stock macOS /bin/bash) under set -u.

2. read_var regex tightened from [[:space:]]*# to [[:space:]]+# so a
   value containing '#' (e.g. hunter2#withhash) isn't silently truncated
   at the first '#'. Standard .env convention: # is a comment only when
   preceded by whitespace.
generate-secrets.sh now reads MIKE_SUPABASE_MODE and MIKE_STORAGE_MODE
and only generates the secrets the chosen modes use. Missing operator-
supplied values (EXTERNAL_SUPABASE_*, R2_* for external storage, etc.)
emit WARN lines so the operator knows what to fill in.

current_value regex tightened ([[:space:]]+# instead of *) to match
the .env convention: '#' is only a comment when preceded by whitespace.
Matches the same fix applied to mike CLI in 5fb9f95.
scripts/smoke-test.sh boots the default stack, waits for Caddy on
$MIKE_PORT, hits the backend, and tears down. README gains a
'Deployment modes' section with the six-combo matrix and the
frontend-rebuild caveat for hosted-Supabase mode.
backend/src/index.ts exposes app.get('/health'); Caddy's handle_path
/backend/* strips the prefix, so the smoke test URL must be
/backend/health, not /backend/healthz.
- init-db.sh: sanitize PG_URL when logging so password doesn't leak
  to 'docker compose logs init-db'. Only affects the PG_URL branch;
  PGHOST branch already used a non-secret CONN_DESC.

- generate-secrets.sh: warn about EXTERNAL_POSTGRES_AUTHENTICATOR_URL
  and EXTERNAL_SUPABASE_GOTRUE_PG_URL in bundled-byo-db mode. Without
  them PostgREST/GoTrue default to the deselected bundled postgres
  host and silently fail to start.

- README.md: use ./mike build mike-frontend consistently (the
  Configuration section still had a stale docker compose form).
storage.test.ts: wrap each test body in try/finally so a failing
assertion doesn't leak env vars to the next test.

test/mike.test.sh: two new cases proving the read_var regex requires
whitespace before '#' (locks in the fix from 5fb9f95). Brings test
count from 8 to 10.

scripts/smoke-test.sh: header comment now states explicitly that the
script only covers the default (bundled-full + bundled) mode.
@Lef-F Lef-F merged commit c1bab0b into main May 12, 2026
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.

1 participant