Database
Connect a PostgreSQL database, apply every migration and create the site owner's account.
The site stores users, sessions, orders and credits in a PostgreSQL database you connect once; it works when this check passes:
pnpm tsx scripts/check-database.ts
Its last line must be Database: all checks passed. Every other integration needs this one first.
You
Create one database and paste its connection string when the agent asks. Then name the email the site owner will sign in with.
| Value | Where you get it |
|---|---|
DATABASE_URL (Neon, recommended) |
console.neon.tech → New project (any name, the region closest to your users) → Create → on the project dashboard click Connect → keep Connection pooling on → copy the connection string. Its host contains -pooler. |
DATABASE_URL (Supabase) |
supabase.com/dashboard → New project (set a database password and keep it) → open the project → Connect (top bar) → Connection string → Transaction pooler (port 6543) → copy, then replace [YOUR-PASSWORD] with your password. |
DATABASE_URL (any other Postgres) |
your provider's connection string: postgres://user:password@host:port/database |
| Owner email | the email you will sign in with as the site owner |
The dashboard labels are as of late 2026 (check the dashboard, menus move). If one has moved, the connection string is always behind the project's Connect button.
On Neon, open the project and click Connect:
Keep Connection pooling on and copy the string:
Give the app the pooled string. ShipAgent recognizes Neon and Supabase hosts and sets the connection options a pooler needs. On Cloudflare the deployed site does not use DATABASE_URL directly: it connects through Hyperdrive, which the deploy step creates from the same string.
Agent
- Writes
DATABASE_URLinto.env.localand runs the check to confirm it connects. - Applies every migration in
lib/db/migrations/withpnpm db:migrate. If the database already holds data, it asks you first. - Never uses
db:push: a database made that way has no migration record and breaks the next migration. - Runs the check again: every table and column exists, the latest migration is applied, and a test write is read back and rolled back.
- Creates the owner account with
pnpm create-superadmin <your email>. That user gets thesuperadminrole and a verified email. - Runs the check until it passes, then moves on to auth.
There is no password. The owner signs in with an email code (after Email) or with Google or GitHub using the same email (after Auth); Google and GitHub accounts are linked to an existing user with the same email.
What a pass looks like
✓ connected to <host> — PostgreSQL <version>
app connection profile: <profile> (ssl <setting>, prepared statements <on|off>)
✓ all <n> tables of lib/db/schema.ts exist with every column
✓ migrations applied up to <latest migration> (<n> in lib/db/migrations)
✓ write + read inside a transaction works (rolled back, nothing kept)
✓ superadmin: you@yourdomain.com
Database: all checks passed
Before the agent migrates, the table lines are ✗; that is expected. The last ✗ to disappear is no superadmin in the user table, which the owner account step fixes.
When it fails
| The check says | What it means | Fix |
|---|---|---|
DATABASE_URL is not a postgres:// connection string |
a dashboard URL was pasted, or the quotes are broken | copy the string again from the provider's Connect panel |
cannot connect … password authentication failed |
wrong password, or Supabase's [YOUR-PASSWORD] is still in the string |
Supabase: Project Settings → Database → reset the password, paste the new string |
cannot connect … timeout or ENOTFOUND |
wrong host, or a paused free-tier project | copy the string again; restore a paused Supabase project from its dashboard first |
cannot connect with profile self-hosted (ssl false) |
the host looked local, so encryption was turned off | the agent adds ?sslmode=require to the URL (or ?sslmode=disable for a plain local Postgres) |
table … does not exist or misses columns |
migrations not applied, or applied to another database | the agent runs pnpm db:migrate against the same DATABASE_URL |
drizzle.__drizzle_migrations does not exist while the tables exist |
the tables were made by hand or with db:push |
fresh database: drop it and migrate; database with data: the agent asks you before touching it |
| the migration fails on Supabase's transaction pooler (port 6543) | the pooler does not suit migrations | the agent runs the migration once with the Session pooler or Direct string and keeps the pooled string in .env.local |
no superadmin in the user table |
the owner step was skipped or ran against another database | tell the agent your email again |
Going live
- Create a separate database for production. Its
DATABASE_URLgoes intocloudflare/.env.production.local(Cloudflare) or the Vercel project's environment variables. - On Cloudflare, the new-site script creates the Hyperdrive config from that string (see the
deploy-cloudflareskill). - The agent migrates production and creates the owner account there from your machine, pointing the same commands at the production string.
- The check must pass against production too:
pnpm tsx scripts/check-database.ts --env cloudflare/.env.production.local.