feat: self-host and air-gap stack — embedded Supabase, one-step Docker, AIRGAPPED mode (ADR) - #36
Open
amal66 wants to merge 1 commit into
Open
feat: self-host and air-gap stack — embedded Supabase, one-step Docker, AIRGAPPED mode (ADR)#36amal66 wants to merge 1 commit into
amal66 wants to merge 1 commit into
Conversation
…r, AIRGAPPED mode Port of the self-host/air-gap stack from amal66/mike@main, translated from the fork's monorepo layout (apps/api, apps/web, supabase/migrations) to the upstream backend/ + frontend/ layout: - docker-compose.yml: local dev stack (api + web hot-reload, MinIO, Redis) - backend/Dockerfile, frontend/Dockerfile (+ .dockerignore): dev/production/ air-gapped image targets; the air-gapped api image adds LibreOffice for offline DOCX→PDF and stages backend/schema.sql as the migration baseline - backend/scripts/migrate.mjs: idempotent, ledgered SQL migration runner (advisory lock, checksum drift detection) replacing the Supabase dashboard for offline installs - airgapped/: embedded Supabase data plane (Postgres + GoTrue + PostgREST behind an nginx gateway), MinIO, Redis, Mailpit, Ollama; gen-secrets.sh (derived JWT triple), bundle/install/backup/restore/acceptance scripts, Caddy TLS front, runbook - backend AIRGAPPED=true env gating: secret boot guard (refuses demo/ placeholder secrets), cloud LLM dispatch refusal, CourtListener + user MCP tool stripping Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design (ADR)
Context. Mike's default deployment leans on external services: hosted Supabase (auth + Postgres + PostgREST), Cloudflare R2, cloud LLM APIs, and Resend. Legal teams routinely handle documents that must not leave their network; some operate fully disconnected ("air-gapped") environments. Upstream's CONTRIBUTING defers local-hosting refactors of the main app to a future fully-local version, and community PRs Open-Legal-Products#196 and Open-Legal-Products#149 (on Open-Legal-Products/mike) already explore Docker-based self-hosting. This PR ports the fork's production self-host/air-gap stack in a way that is strictly opt-in: nothing changes for a deployment that doesn't set
AIRGAPPED=trueor use the new compose files.Decision.
airgapped/gateway.conf) that routes the two prefixes supabase-js uses (/auth/v1,/rest/v1). Kong/Realtime/Storage/Studio/edge functions are dropped — the security boundary is the JWT + deny-all RLS, not a gateway apikey gate.AIRGAPPED=truegates every egress channel the backend owns: cloud LLM dispatch is refused with an attributedModelUnavailableErrorbefore any request leaves; CourtListener and user MCP connector tools are stripped from the chat tool set; and a boot guard (assertSecretsHardened) refuses to start on the Supabase demo JWT secret, demo-issuer keys, keys not signed byJWT_SECRET, or placeholder/short encryption secrets. Network isolation is the other half; the code half means an imperfect network setup still can't silently leak content.gen-secrets.shmints a CSPRNG JWT secret and derives the anon/service_role JWTs from it so GoTrue, PostgREST, and supabase-js agree; the compose uses${VAR:?}so a missing secret fails the boot loudly instead of falling back to demo values.backend/scripts/migrate.mjsapplies ordered SQL under a Postgres advisory lock, records each file in aschema_migrationsledger with a checksum, no-ops on re-run, and fails on checksum drift. Perbackend/schema.sql's own header, a fresh database takesschema.sql(staged in the air-gapped image as the lexically-first baseline "migration"), not the dated incremental files.bundle.sh(connected host) pulls pinned images + builds app images into a checksummed tarball;install.sh(air-gapped host) verifies, loads, generates secrets once, and boots in dependency order;backup.sh/restore.shcapture the three stateful stores and the secrets (encrypted data is undecryptable without them);acceptance.shruns the executable checks and lists the operator-side ones honestly.Consequences. A one-step
docker compose updev stack (API + web hot-reload + MinIO + Redis) and a fully offline production stack become possible. Cost: this repo now carries compose files, two Dockerfiles, and pinned third-party image tags that need a re-bundle cadence (documented inairgapped/OPERATIONS.md). On this branch alone,AIRGAPPED=truerefuses all chat models — local model serving (Ollama) arrives with the provider-registry/local-LLM PRs, whose env vars (ENABLE_OLLAMA,OPENAI_BASE_URL,AIRGAP_DEFAULT_MODEL)gen-secrets.shalready emits.Alternatives considered.
Summary
Ports the fork's self-host and air-gap stack: a one-step Docker dev environment, an embedded-Supabase production stack for disconnected networks, generated (never demo) secrets with a boot guard, an idempotent migration runner, and
AIRGAPPED=trueegress gating in the backend.Changes
docker-compose.yml— local dev stack: api (tsx hot-reload) + web (Next dev) + MinIO + Redis, with named node_modules volumes.backend/Dockerfile,frontend/Dockerfile, per-dir.dockerignore— dev / build / production targets;production-airgappedadds LibreOffice (+fonts) for offline DOCX→PDF and stagesschema.sql+ the migration runner.backend/scripts/migrate.mjs— ledgered, advisory-locked, checksum-verified SQL runner (new dep:pg, which the fork ships).airgapped/—docker-compose.airgapped.yml(Postgres + db-init + GoTrue + migrate + PostgREST + nginx gateway + MinIO + Redis + Mailpit + Ollama + api + web, only the gateway host-published),gateway.conf,Caddyfile(internal-CA TLS front),gen-secrets.sh,bundle.sh,install.sh,backup.sh,restore.sh,acceptance.sh,README.md,OPERATIONS.md, test override.lib/airgap.ts(isAirgapped()),lib/secretGuard.ts+ boot call inindex.ts, cloud-LLM dispatch refusal inlib/llm/index.ts, CourtListener/MCP tool stripping inlib/chat/streaming.ts.lib/__tests__/secretGuard.test.ts,__tests__/integration/stack.supabase.test.ts(env-gated stack E2E: auth contract, RLS deny-all, tenant isolation, leak sweep over all 24 public tables)..gitignore: never commitairgapped/.env.generated;tsconfig.json: exclude tests from the production build (same hunk as the local-LLM PR).Why
Law firms and courts have data that cannot leave the building; several will only evaluate Mike if it runs entirely on their hardware. Upstream's CONTRIBUTING defers local-hosting refactors to a future fully-local version — this PR is shaped for that constraint: every file is opt-in (new compose files, a new
airgapped/directory, code paths dead unlessAIRGAPPED=true), and a non-Docker deployment is byte-for-byte unaffected at runtime. It is fully self-contained — nothing in it depends on any other PR, open or closed; it ships the complete stack on its own. It is also deliberately non-competing with Open-Legal-Products#196 and Open-Legal-Products#149 — see the Alternatives section; the offer to reconcile with those PRs' authors is genuine, and the air-gap layer here composes with either of their compose approaches.Testing
backend:npm install+npm run build(tsc) — green as committed.upstream-pr/test-harnessbranch installed locally, not committed):secretGuard.test.ts9/9 passed;stack.supabase.test.ts4 skipped as designed (env-gated — it runs against a live stack viaacceptance.sh/ the README verify flow).gen-secrets.shruns and emits a consistent derived-JWT env;docker compose configvalidates bothdocker-compose.airgapped.yml(+ test override, with generated secrets) and the rootdocker-compose.yml;bash -non all six scripts;node --checkonmigrate.mjs.airgapped/README.mdrecords its live verification (fresh volume → auth healthy, baseline applies idempotently, stack-E2E 4/4).Provenance
All added lines are mechanical ports of amal66/mike@
origin/main(b3166dd); exceptions (all layout translations or upstream-compat renames, no new logic):apps/api→backend,apps/web→frontend; the fork's workspace-based Dockerfiles (root-contextnpm ci --workspace=…) became standalone per-directory Dockerfiles, and the fork's single root.dockerignorebecame per-directory ones.supabase/migrations/(its squashed baseline); upstream keeps the baseline inbackend/schema.sql, so the air-gapped image stagesschema.sqlas00000000000000_baseline.sqlandmigrate.mjs's default dir/comments were adjusted accordingly.secretGuard.ts: enforcement narrowed toAIRGAPPED=trueonly — the fork also enforces onNODE_ENV=production, which could break existing upstream cloud deployments (e.g.MCP_CONNECTORS_ENCRYPTION_SECRETis legitimately optional upstream); one test assertion added to pin the narrowed behavior.llm/index.ts): the fork enforces the same rule via its provider registry (cloud providers not registered +assertModelAvailable);ModelUnavailableErrorand its message are copied from the fork.gen-secrets.sh:NEXT_PUBLIC_SUPABASE_ANON_KEY→NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY,R2_BUCKET→R2_BUCKET_NAME(upstream's variable names).stack.supabase.test.ts:PUBLIC_TABLESregenerated from upstream'sschema.sql(addslibrary_folders).backend/tsconfig.jsontest exclusion — identical to the hunk on the local-LLM branch.pg@^8.22.0(a dep the fork ships) + lockfile update.airgapped/README.md/OPERATIONS.md: verify-flow paths, "Web CDN egress removed" downgraded from ✅ toCredits & prior art
(Expanding the Alternatives section above into explicit credit.)
anon/authenticatedon every public table) follows fix: enable RLS with deny-all policy on all public tables Open-Legal-Products/mike#145; the fork's RLS migration was adapted from that PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC