think
16px
820px

Shared Postgres cutover (group 1) — completion notes

Date: 2026-08-26 · Commit: 029b5e5 · Live on ahu-int-01
Spec: docs/superpowers/specs/2026-08-26-shared-postgres-state-design.md

Staff multi-turn — checked first, needs nothing

The Agno agent already carries conversation history
(add_history_to_context, num_history_runs=5). Verified behaviourally on the
live console rather than from config:

turn result
"Berapa jumlah pendirian PT tahun 2024?" 31.569
"Bagaimana dengan 2025?" 36.728 — and volunteered "dibandingkan 2024 (31.569), naik 16,3%" unprompted
"Berapa selisih antara keduanya?" table with both years, 5.159, +16,3%

Turn 3 required both earlier numbers. A first scrape showed turn 2 as an empty
reasoning header; that was a 12-second settle window, not a bug — with 30
seconds the full answer and its chart were there.

The cutover

public_users, public_threads, public_thread_messages now live in schema
chatbot on ahu-dash-db. Both apps read the same rows regardless of host.

Proof, by performing the operation that failed silently on 24 Aug:

admin resets a password in Konsol Admin
  -> login with the temp password on the PUBLIC surface : SUCCEEDS
  -> the old password                                   : rejected
admin disables the account
  -> login on the PUBLIC surface                        : rejected

All four previously touched a file the other app never read. The console also
now lists an account created seconds earlier through the public surface — 11
of 11, including the new one.

Data: 10 users / 7 threads / 24 messages copied, 3 soft-deletes preserved.
Two orphaned messages skipped (their thread did not exist even in SQLite; the
FK would have rejected them).

The check that mattered: after cutover, Postgres took a new registration
and went to 11 while SQLite stayed at 10. Row counts alone would only have
proved a copy happened, not that writes had actually moved.

Things that only surfaced by deploying

Nothing ran the migrations. They existed and no code called them. Both apps
would have booted against a database with no tables and served 500s on login,
registration, and history. Now applied on boot from instrumentation.ts, and
deliberately FATAL on failure — a container that reports healthy with no schema
is harder to notice than a deploy that visibly did not start.

pg cannot be imported by a standalone script in the runtime image. It is
webpack-bundled into the Next server output and is not on disk as a package, so
the Node migration script died on ERR_MODULE_NOT_FOUND. Rewritten in Python:
sqlite3 is stdlib and psycopg installs from a wheel in seconds, so it runs
in a throwaway container needing nothing from the app image.

Next compiles instrumentation.ts for the edge runtime too. Webpack
resolves imports statically, so importing the db package there failed the build
on Can't resolve 'fs' even behind a NEXT_RUNTIME check.
serverExternalPackages did not help — that governs the server bundle, not the
edge compile. Guarding a dynamic import of a separate module is the shape
webpack understands.

CREATE SCHEMA IF NOT EXISTS is not concurrency-safe. Two sessions both
pass the check and one dies on the pg_namespace unique index. Both apps
migrate on boot and compose starts them together, so on a cold database that
was a coin flip on a container crashing. Caught by a test before deploying; the
advisory lock now precedes every CREATE.

PublicAccountAdminStore was nearly left behind. The group-1 commit claimed
it moved and it had not. Left that way, the state would have been worse than
the original divergence — public-web on Postgres, the console still on
/data/public.sqlite. Found by grepping for remaining better-sqlite3 imports
rather than trusting the commit message.

What is still SQLite, on purpose

store why
staff_users, audit written and read by ONE app — no sharing problem to solve
policies, config group 2: cross-app, same latent hazard, not yet migrated

Group 2 has not bitten only because nobody edited a policy between 24 and 26
Aug. Any console policy or orchestrator-config edit still fails to reach the
public surface. That is the next piece of work.

Rollback

Backups were taken through the SQLite backup API, not cp — a live file
copy can catch a torn page:

/data/audit.sqlite.pre-pg-20260826-120718
/data/config.sqlite.pre-pg-20260826-120718
/data/policy.sqlite.pre-pg-20260826-120718
/data/public.sqlite.pre-pg-20260826-120718
/data/staff.sqlite.pre-pg-20260826-120718

The originals were never modified and still hold the pre-cutover rows. Rollback
is the previous image plus those files.

Running the DB tests

They skip without a database and say so loudly, so pnpm check stays green on
a machine with no server without a disabled guard passing for a working one:

docker run -d --name ahu-test-pg -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test \
  -e POSTGRES_DB=test -p 55917:5432 postgres:16-alpine
TEST_DB_URL=postgres://test:test@localhost:55917/test pnpm check

501 tests with a database.