Postgres Adopt
The engine is the brain's foundation: PGLite is the zero-config floor,
Postgres is where concurrency, multi-machine access, and 1000+ pages live.
This skill answers "which one am I on?", prefers Postgres when the
operator wants it, and moves data safely — never by flipping config.
Contract
This skill guarantees:
- Detection is engine-free and read-only:
gbrain engine status --json
answers with the database down (that is the point of the command).
- Engine changes NEVER happen by editing config. A PGLite brain moves with
gbrain migrate --to postgres (alias --to supabase), which graduates it:
plan first (exit 3), run only with the user's --yes --expect <plan_hash>,
verify every table, then flip routing. This skill wraps it; it never
reimplements it.
- The target URL never appears in a command, transcript or output: it lives
in
GBRAIN_TARGET_URL and every command uses --url-env GBRAIN_TARGET_URL.
- Provisioning consent is explicit: the docker rung needs
--allow-docker,
creating a database on a local server needs --allow-create-db. Headless
mutation of infrastructure the operator didn't opt into never happens.
Step 1 — Detect
gbrain engine status --json
Branch on the output:
effective_engine: "postgres" and (optionally) --probe says ok →
report healthy, done.
effective_engine: "postgres" but the probe fails → this is an ACCESS
problem, not an adoption problem: route to db-repair.
config_file_engine differs from effective_engine → an env URL is
overriding the config file; tell the operator which one wins — the signal
is db_url_source (env:GBRAIN_DATABASE_URL / env:DATABASE_URL means
env wins; env.note additionally fires when both env URLs are set or the
cwd-.env shadow guard excluded one).
thin_client: true → the brain lives on a remote server; engine choices
belong to that host. Stop.
effective_engine: null (no brain) → Step 2.
effective_engine: "pglite" with data → Step 3.
- The output has a
graduation block whose state is not none (in progress,
interrupted, graduated or split) → Step 4.
Step 2 — Fresh install, Postgres-first
gbrain init --prefer-postgres
The ladder tries, in order: an env URL → Supabase Management-API discovery
(SUPABASE_ACCESS_TOKEN, plus SUPABASE_PROJECT_REF on multi-project
accounts and SUPABASE_DB_PASSWORD for the connection string) → a local
Postgres (only when PGHOST/PGPORT/PGUSER/PGPASSWORD are set or
--local-postgres is passed) → docker → PGLite. Each unusable rung prints a
one-line note and falls through; nothing is silent.
- Ask the operator BEFORE adding
--allow-docker (it creates and owns a
gbrain-postgres container that survives reboots) or --allow-create-db
(it runs CREATE DATABASE on their local server).
--json reports {engine, ladder_rung, url_source} — relay which rung won.
- If the ladder lands on PGLite, that is a fine outcome: say so, and note the
upgrade path below is available whenever they want it.
Step 3 — Existing PGLite brain: plan, ask, run
Full reference: Move a PGLite brain to Postgres.
- Target URL. Ask the user to put the connection string in an
environment variable without pasting it into the conversation
(
read -rs GBRAIN_TARGET_URL && export GBRAIN_TARGET_URL). On Supabase use
the transaction pooler; on an IPv4-only host also set
GBRAIN_DIRECT_DATABASE_URL to the session pooler.
- Plan. This changes nothing and exits 3 with the
confirmation_required document:
gbrain migrate --to postgres --url-env GBRAIN_TARGET_URL --json
Relay user_message, what moves, what stays on this computer, blockers
and the estimate. Stop and wait for the user. A blocker with its own
fix gets cleared first (ask when it says ask_user).
- Run, after the user agrees. Use
fix.command from the document:
gbrain migrate --to postgres --url-env GBRAIN_TARGET_URL --yes --expect <plan_hash>
preview_changed means the plan changed: go back to step 2. For thousands
of pages run it in the background and poll gbrain migrate --status --json.
- Report. Relay the target doctor verdict from the run's output, that
existing tokens, OAuth clients and local writers stay valid, the
gbrain mcp expose follow-up for other machines, and, if a serve handed
the brain over, that the user restarts the MCP client.
- A
gbrain serve holding the brain hands it over on its own; never ask the
user to stop it first. graduation_source_writer_held names the process to
stop when one does not.
- The old data dir is kept as
<path>.graduated-<run_id> (doctor's
pglite_leftovers reports it). Deleting it is the user's call.
gbrain doctor's pglite_scale check warns at 1000+ pages and points at
the plan command — that warning is this skill's cue.
Step 4 — A move already underway
gbrain migrate --status --json
It is read-only and names the next command for the state. Exit 75 or a
run in progress → wait and poll. Interrupted → gbrain migrate --resume
(or gbrain migrate --rollback-to-source if the user wants to stay on
PGLite). Every refusal names its fix; the recovery table is in the guide.
A rollback that lists data loss runs only with the user's
--rollback-to-source --yes --expect <hash>.
The tradeoff (say it when recommending)
Postgres wins on concurrency, multi-machine access, and scale. PGLite keeps
the per-turn bootstrap hook lane (hook injection is PGLite-only today —
docs/guides/bootstrap.md); on Postgres, ambient context rides
MCP-every-session and the pull protocol instead. Recommend Postgres when the
operator has concurrent agents, multiple machines, or a 1000+ page brain;
otherwise PGLite is genuinely fine.
When it fails
Follow the agent operator protocol for any gbrain error code, exit code, [AGENT] block or notice block. Specific to this skill:
effective_engine: "postgres" but the probe fails: this is an access problem; run gbrain engine status --probe, then the db-repair skill.
- Provisioning steps (Docker, installing Postgres) and the move itself need the user's explicit agreement; the docker rung needs
--allow-docker, the move needs --yes --expect <plan_hash> after the user saw the plan.
gbrain config set engine … is refused by design: switch engines through the migrate path, never by editing config.
Anti-Patterns
- NEVER
gbrain config set engine ... — it is refused by design; an engine
flip without a data migration splits the brain across two stores.
- NEVER pick Postgres over a healthy PGLite brain without the migrate path.
- NEVER add
--yes before the user has seen the plan, and never use
--force unless the plan listed what it wipes and the user agreed.
- NEVER put the database URL or password in a command line; use
--url-env GBRAIN_TARGET_URL.
- NEVER run the docker rung without the operator's explicit yes.
- NEVER paste or echo
SUPABASE_ACCESS_TOKEN / passwords into output.
Output Format
Detection reports in one line; changes report in 2-4:
Engine: <pglite|postgres> (source: <db_url_source>) [probe: ok, 42ms]
Action: <none | init rung that won | migrate --to postgres result>
Next: <upgrade note, or "healthy — nothing to do">
Quote gbrain engine status output as-is (it is already redacted); name the
winning ladder rung when an install ran; after a migration, include the
target's gbrain doctor verdict.