Skip to content

feat: scaffold the career-forge app and development environment - #15

Merged
BohnBawerick merged 12 commits into
mainfrom
fm/cf-scaffold-signoff
Aug 24, 2026
Merged

feat: scaffold the career-forge app and development environment#15
BohnBawerick merged 12 commits into
mainfrom
fm/cf-scaffold-signoff

Conversation

@BohnBawerick

@BohnBawerick BohnBawerick commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Intent

Build the career-forge skeleton so every later ticket has somewhere to put code. This is the first code in a repo that was documentation only. The specification is GitHub issue #13 (BohnBawerick/career-forge); its "Deliver" list is the contract. CONTEXT.md and docs/adr/ fix the vocabulary and the stack, and must be used.

Nothing here is a design decision. Issue #7, ADR 0004 and ADR 0005 settled the stack: TypeScript end to end in one Nuxt 4 app, Nitro server routes as the HTTP API, plain Postgres with GoTrue beside it, Drizzle for queries and migrations, pg-boss for the queue, filesystem storage behind a module with an unimplemented S3 switch, one package with no workspace.

Definition of done from the ticket: docker compose -f docker-compose.dev.yml up starts Postgres and GoTrue; pnpm dev serves an empty Nuxt app on localhost:3000 that can sign a fabricated Account up and log it back in; pnpm worker picks a test job off pg-boss. All three must be proven with real output in the PR body.

Deliver exactly this:

  • One package at the root with app/, server/, core/, db/, queue/, worker/, seed/, tests/, e2e/. server/ and worker/ both import core/; core/ imports neither, and a lint rule enforces that (not a convention). The rule must be shown failing on a deliberate violation before that violation is removed.
  • docker-compose.dev.yml with postgres and gotrue only.
  • docker-compose.yml with all five services; it may reference a Dockerfile that does not build yet.
  • .env.example naming every variable, with no real value in it.
  • Drizzle wired to Postgres with a first migration, plus a check that reading GoTrue's auth schema works.
  • pg-boss wired, with every send behind a single queue/ module.
  • Vitest and Playwright each installed with one passing test.
  • A storage/ module with a filesystem driver and an S3 switch that is not implemented yet.

Explicitly not in this ticket: any domain logic, any AI call, any real screen, the production Dockerfile, Caddy.

One accepted exception to "no domain logic", decided by the author after review: four ADR 0007 invariants are enforced in code this ticket already ships, because ADR 0007 already settles them as doctrine and two of them break the ticket's own acceptance (a sign-up that bricks a claimed install, and a failed first sign-up that leaves an install permanently ownerless). Exactly these four, and nothing more: the sign-up rollback is scoped to the account write, so a failed token grant no longer deletes the only login; the login repair path claims ownership when no Owner exists; a unique partial index on is_owner where is_owner is true is declared in db/schema.ts and generated into the first migration, so two concurrent sign-ups cannot both become Owner; and pnpm seed is guarded by the same installIsUnclaimed check the HTTP route enforces. No accounts subsystem, no new abstraction, no invite or membership work, no new module. The accounts ticket still owns everything else, and the pull request body says so.

Sign-off, decided by the author after three runs deadlocked on it: the pipeline owns its own commits and they must not be rewritten. Do not amend, rebase, reword or otherwise rewrite any commit on this branch, do not force anything, and do not raise a missing per-commit Signed-off-by trailer as a finding. CONTRIBUTING.md requires the Developer Certificate of Origin and defines its point as what goes into the public git history. This repository has no automated per-commit sign-off check. The pull request will be squash merged, so exactly one commit reaches public history and the author puts the Signed-off-by trailer in that squash message. Unsigned intermediate commits on the branch never land anywhere public, so the rule is satisfied in the only place it is defined to matter. The pull request body states this reasoning.

The branch is fm/cf-scaffold-signoff. An earlier branch name was abandoned because adding sign-off trailers rewrote commit messages and left that name pinned to a head no push could fast-forward past. Neither name was ever pushed to origin. The pull request body says the branch was renamed for that reason.

One addition beyond the ticket, deliberate: a GitHub Actions workflow at .github/workflows/ci.yml, because the repository has no CI at all and later tickets would otherwise validate blind. Keep it proportionate to what exists: lint, typecheck, the Vitest suite, and a production build. Wire the Playwright test into CI only if it runs reliably headless there; it does, because the front page renders with no database, so it is included.

Constraints specific to this repository:

  • The repo is public. Nothing committed and nothing in the PR may contain personal data, private paths, or references to other projects. Seed data must be fabricated.
  • LOCAL.md is gitignored and holds pointers to private repositories. Never read from it into anything written, and never un-ignore it.
  • Follow CONTRIBUTING.md, including the DCO sign-off line on commits using the repository's configured git identity. Never add an agent as a commit co-author.
  • The project is AGPL-3.0-or-later. Respect whatever licence-header convention CONTRIBUTING.md sets; do not invent one. CONTRIBUTING.md sets none, so no per-file headers.
  • Write prose the way this repo writes prose: plain hyphens and never em dashes, no title-case headings, no AI vocabulary.

Acceptance criteria:

  1. The three commands in the ticket's done-when sentence all work, demonstrated with real output.
  2. The core/ import rule is enforced by lint, shown failing on a deliberate violation before that violation is removed.
  3. CI is green on the PR.
  4. A stranger can clone the repo and reach a running dev environment using only what is written down; the README carries a getting started section for that.

The stack is settled, so a genuine design fork should not arise. If one does, do not silently pick: raise it as a decision rather than answering it.

What Changed

  • Adds the single-package Nuxt 4 skeleton with app/, server/, core/, db/, queue/, worker/, seed/, tests/ and e2e/, plus an ESLint rule in eslint.config.mjs that fails on any import from core/ into server/ or worker/ (ADR 0004).
  • Wires the settled stack: docker-compose.dev.yml with postgres and gotrue, docker-compose.yml with all five services, .env.example with no real values, Drizzle against Postgres with a first account migration and a db/check-auth.ts read of GoTrue's auth schema, pg-boss with every send behind queue/, a storage/ filesystem driver with an S3 switch that refuses, Vitest and Playwright each with a passing test, and a .github/workflows/ci.yml running lint, typecheck, tests, build and the browser test.
  • The front page signs a first Account up and logs it back in through Nitro routes against GoTrue. Four ADR 0007 invariants ship with it: sign-up rollback scoped to the account write, the login repair path claiming an ownerless install, a unique partial index on is_owner, and pnpm seed guarded by the same installIsUnclaimed check the HTTP route enforces.

Notes

  • No other accounts work is here. Invites, membership and the rest stay with the accounts ticket.
  • Commits on this branch carry no Signed-off-by trailer. This repository has no automated per-commit sign-off check, the pull request is squash merged, and the author puts the trailer in the squash message, so the one commit that reaches public history is signed off as CONTRIBUTING.md defines it.
  • The branch was renamed to fm/cf-scaffold-signoff. Adding trailers to an earlier branch rewrote its commit messages and pinned that name to a head no push could fast-forward past. Neither name was ever pushed to origin.

Risk Assessment

✅ Low: The fix-round delta is small and closes all three prior findings at a real shared boundary, verified against the installed drizzle-orm source and the generated migration, with behavior tests that drive the actual functions; the two remaining items are cosmetic error-reporting and dead-export cleanup.

Testing

I ran the ticket's three done-when commands for real on a fresh Postgres volume and captured product-level output for each. The dev compose brings up postgres and gotrue only, both healthy; migrations apply and pnpm db:check-auth reads GoTrue's auth schema. In a real Chrome session against pnpm dev I claimed the install as a fabricated Owner, signed out (the button switches from "Claim this install" to "Sign in", so sign-up has closed), and logged back in - five screenshots plus a GIF of the flow. pnpm worker picked up a pnpm queue:ping job with matching ids on both sides and the pg-boss row ends up completed. The core/ import rule was shown failing on a deliberate violation and the violation removed. Vitest (34 tests) and Playwright (1 test) both pass, and I additionally exercised the four accepted ADR 0007 invariants against live state: the database rejects a second Owner via the partial unique index, pnpm seed refuses a claimed install and seeds a fresh one, the HTTP signup route answers 403 once claimed, and deleting the account row behind a live GoTrue login lets the next login repair it and claim Owner. The one invariant not fault-injected is the scoped sign-up rollback on a failed token grant; it is only readable in server/api/auth/signup.post.ts, but its worst consequence (a permanently ownerless install) is covered end to end by the repair demonstration. I did not run pnpm build or the CI workflow - those belong to the later phases. Afterwards I tore the compose stack and its volume down and removed .env, .data and test output, leaving the worktree clean.

  • Evidence: Front page on an unclaimed install
  • Evidence: Sign-up form filled with a fabricated Account
  • Evidence: Signed in as the Owner after sign-up
  • Evidence: Signed out, and sign-up has closed (button reads Sign in)
  • Evidence: Logged back in as the same Owner
  • Evidence: Sign-up and log-back-in flow (animated)
Evidence: Dev environment up: two services, GoTrue health, migrations, auth schema read

$ docker compose -f docker-compose.dev.yml config --services postgres gotrue $ docker compose -f docker-compose.dev.yml ps SERVICE IMAGE STATUS PORTS gotrue supabase/gotrue:v2.186.0 Up 15 minutes (healthy) 127.0.0.1:9999->9999/tcp postgres postgres:17-alpine Up 16 minutes (healthy) 127.0.0.1:55432->5432/tcp $ curl -s http://127.0.0.1:9999/health
{"version":"v2.186.0","name":"GoTrue","description":"GoTrue is a user registration and authentication API"} $ pnpm db:migrate Migrations applied. $ pnpm db:check-auth auth schema present, auth.users readable, 1 login(s) in it. 69cde08c-25f5-4285-8256-893be71190fe owner@example.com $ docker compose -f docker-compose.yml config --services (the full stack, five services) postgres worker gotrue web caddy

$ docker compose -f docker-compose.dev.yml config --services
postgres
gotrue

$ docker compose -f docker-compose.dev.yml ps
SERVICE    IMAGE                      STATUS                    PORTS
gotrue     supabase/gotrue:v2.186.0   Up 15 minutes (healthy)   127.0.0.1:9999->9999/tcp
postgres   postgres:17-alpine         Up 16 minutes (healthy)   127.0.0.1:55432->5432/tcp

$ curl -s http://127.0.0.1:9999/health
{"version":"v2.186.0","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}

$ pnpm db:migrate
$ tsx db/migrate.ts
Migrations applied.

$ pnpm db:check-auth
$ tsx db/check-auth.ts
auth schema present, auth.users readable, 1 login(s) in it.
  69cde08c-25f5-4285-8256-893be71190fe  owner@example.com

$ docker compose -f docker-compose.yml config --services    (the full stack, five services)
postgres
worker
gotrue
web
caddy
Evidence: Worker picks a test job off pg-boss

shell 1 $ pnpm worker Worker listening on career-forge.test. Ctrl-C to stop. Picked test job 8873aebf-02c4-48a8-9360-afd2ecb2ae4b: "a fabricated test job from the scaffold check" sent 2026-08-24T02:35:06.515Z, handled 2026-08-24T02:35:08.497Z shell 2 $ pnpm queue:ping "a fabricated test job from the scaffold check" Sent test job 8873aebf-02c4-48a8-9360-afd2ecb2ae4b: a fabricated test job from the scaffold check

shell 1
$ pnpm worker
$ tsx worker/index.ts
Worker listening on career-forge.test. Ctrl-C to stop.
Picked test job 8873aebf-02c4-48a8-9360-afd2ecb2ae4b: "a fabricated test job from the scaffold check" sent 2026-08-24T02:35:06.515Z, handled 2026-08-24T02:35:08.497Z

shell 2
$ pnpm queue:ping "a fabricated test job from the scaffold check"
$ tsx queue/ping.ts 'a fabricated test job from the scaffold check'
Sent test job 8873aebf-02c4-48a8-9360-afd2ecb2ae4b: a fabricated test job from the scaffold check
Evidence: pg-boss job row after the worker handled it

id | name | state | note | completed --------------------------------------+-------------------+-----------+-----------------------------------------------+----------- 8873aebf-02c4-48a8-9360-afd2ecb2ae4b | career-forge.test | completed | a fabricated test job from the scaffold check | t

                  id                  |       name        |   state   |                     note                      | completed 
--------------------------------------+-------------------+-----------+-----------------------------------------------+-----------
 2902e99d-11cc-413a-be91-450b8405ddec | career-forge.test | completed | a fabricated test job from the scaffold check | t
 bc6e8bf1-48e5-436e-8640-1cca10aa18fe | career-forge.test | completed | a fabricated test job from the scaffold check | t
 8873aebf-02c4-48a8-9360-afd2ecb2ae4b | career-forge.test | completed | a fabricated test job from the scaffold check | t
(3 rows)
Evidence: The core/ import rule fails on a deliberate violation

$ cat core/deliberate-violation.ts import { QUEUES } from '../worker/index' export const wrong = QUEUES $ pnpm lint /.../core/deliberate-violation.ts 2:1 error '../worker/index' import is restricted from being used by a pattern. core/ imports neither server/ nor worker/ (ADR 0004). Both of those import core/, not the other way round no-restricted-imports ✖ 1 problem (1 error, 0 warnings) exit=1

$ cat core/deliberate-violation.ts
// Deliberate ADR 0004 violation, added only to show the lint rule fires. Removed straight after.
import { QUEUES } from '../worker/index'

export const wrong = QUEUES

$ pnpm lint
$ eslint .

core/deliberate-violation.ts
  2:1  error  '../worker/index' import is restricted from being used by a pattern. core/ imports neither server/ nor worker/ (ADR 0004). Both of those import core/, not the other way round  no-restricted-imports

✖ 1 problem (1 error, 0 warnings)

[ELIFECYCLE] Command failed with exit code 1.
exit=1
Evidence: pnpm seed is guarded by the same installIsUnclaimed check as the route

$ pnpm seed # the install has already been claimed by the browser sign-up Error: This install has an Owner already, so there is nothing to seed. Drop the volume and start again to seed a fresh one. exit=1 --- reset the install, then seed a fabricated Owner --- $ pnpm seed Seeded Owner owner@example.com (86436185-a65f-4720-b210-d57584342475). email | is_owner -------------------+---------- owner@example.com | t

--- seed refuses once someone has claimed the install (ADR 0007 guard) ---
$ pnpm seed          # the install has already been claimed by the browser sign-up
$ tsx seed/index.ts
Error: This install has an Owner already, so there is nothing to seed. Drop the volume and start again to seed a fresh one.
    at main (seed/index.ts:30:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
[ELIFECYCLE] Command failed with exit code 1.
exit=1

--- reset the install, then seed a fabricated Owner ---
$ psql -c "truncate table account" -c "delete from auth.users"
TRUNCATE TABLE
DELETE 1
$ pnpm seed
$ tsx seed/index.ts
Seeded Owner owner@example.com (86436185-a65f-4720-b210-d57584342475).
Password: forge-a-better-password

$ psql -c "select email, is_owner from account"
       email       | is_owner 
-------------------+----------
 owner@example.com | t
(1 row)
Evidence: The database refuses a second Owner

$ psql -c "insert into account (auth_user_id, email, is_owner) values (gen_random_uuid(), 'second.owner@example.invalid', true)" ERROR: duplicate key value violates unique constraint "account_one_owner" DETAIL: Key (is_owner)=(t) already exists. $ psql -c "insert into account (..., 'member@example.invalid', false)" INSERT 0 1

--- the database refuses a second Owner (unique partial index account_one_owner) ---
$ psql -c "insert into account (auth_user_id, email, is_owner) values (gen_random_uuid(), 'second.owner@example.invalid', true)"
ERROR:  duplicate key value violates unique constraint "account_one_owner"
DETAIL:  Key (is_owner)=(t) already exists.

--- a non-Owner row is still allowed, so the index only bites on is_owner = true ---
$ psql -c "insert into account (auth_user_id, email, is_owner) values (gen_random_uuid(), 'member@example.invalid', false)"
INSERT 0 1
         email          | is_owner 
------------------------+----------
 owner@example.com      | t
 member@example.invalid | f
(2 rows)
Evidence: HTTP API transcript: status, signup closed with 403, login, me, logout

$ curl -s $BASE/api/auth/status { "unclaimed": false } $ curl -s -X POST $BASE/api/auth/signup (install already claimed) { "statusCode": 403, "statusMessage": "This install has an Owner already. Ask them for an Invite." } HTTP 403 $ curl -s -c jar -X POST $BASE/api/auth/login (the seeded fabricated Owner) { "account": { "id": "86436185-...", "email": "owner@example.com", "isOwner": true } } HTTP 200 $ curl -s -b jar $BASE/api/auth/me { "account": { "id": "86436185-...", "email": "owner@example.com", "isOwner": true } } $ curl -s -b jar -X POST $BASE/api/auth/logout ; curl -s -b jar $BASE/api/auth/me { "ok": true } { "account": null }

--- the HTTP API, against a dev server on 127.0.0.1:3300 ---

$ curl -s $BASE/api/auth/status
{
  "unclaimed": false
}

$ curl -s -o /dev/null -w "%{http_code}" -X POST $BASE/api/auth/signup  (install already claimed)
{
  "error": true,
  "url": "http://127.0.0.1:3300/api/auth/signup",
  "statusCode": 403,
  "statusMessage": "This install has an Owner already. Ask them for an Invite.",
  "message": "This install has an Owner already. Ask them for an Invite.",
  "stack": [
    "This install has an Owner already. Ask them for an Invite.",
    "at createError (node_modules/.pnpm/h3@1.15.11/node_modules/h3/dist/index.mjs:71:15)",
    "at Object.handler (server/api/auth/signup.post.ts:23:0)",
    "at process.processTicksAndRejections (node:internal/process/task_queues:103:5)",
    "at async node_modules/.pnpm/h3@1.15.11/node_modules/h3/dist/index.mjs:2017:19)",
    "at async Object.callAsync (node_modules/.pnpm/unctx@2.5.0/node_modules/unctx/dist/index.mjs:72:16)",
    "at async Server.toNodeHandle (node_modules/.pnpm/h3@1.15.11/node_modules/h3/dist/index.mjs:2316:7)"
  ]
}
HTTP 403

$ curl -s -c jar -X POST $BASE/api/auth/login   (the seeded fabricated Owner)
{
  "account": {
    "id": "86436185-a65f-4720-b210-d57584342475",
    "email": "owner@example.com",
    "isOwner": true
  }
}
HTTP 200

$ curl -s -b jar $BASE/api/auth/me
{
  "account": {
    "id": "86436185-a65f-4720-b210-d57584342475",
    "email": "owner@example.com",
    "isOwner": true
  }
}

$ curl -s -b jar -X POST $BASE/api/auth/logout ; curl -s -b jar $BASE/api/auth/me
{
  "ok": true
}
{
  "account": null
}
Evidence: Storage: filesystem driver round-trips, s3 switch refuses

$ STORAGE_DRIVER=filesystem npx tsx cf-storage-demo.mts STORAGE_DRIVER=filesystem selects driver "filesystem" put ok exists true get a fabricated Source $ find .data/storage -type f && cat .data/storage/sources/2026/fabricated.txt .data/storage/sources/2026/fabricated.txt a fabricated Source $ STORAGE_DRIVER=s3 npx tsx cf-storage-demo.mts STORAGE_DRIVER=s3 selects driver "s3" refused: The S3 storage driver is not implemented yet. Set STORAGE_DRIVER=filesystem, or send a patch.

$ STORAGE_DRIVER=filesystem npx tsx cf-storage-demo.mts
STORAGE_DRIVER=filesystem selects driver "filesystem"
put    ok
exists true
get    a fabricated Source

$ find .data/storage -type f && cat .data/storage/sources/2026/fabricated.txt
.data/storage/sources/2026/fabricated.txt
a fabricated Source

$ STORAGE_DRIVER=s3 npx tsx cf-storage-demo.mts
STORAGE_DRIVER=s3 selects driver "s3"
refused: The S3 storage driver is not implemented yet. Set STORAGE_DRIVER=filesystem, or send a patch.
Evidence: Login repairs a half-finished sign-up and claims Owner

--- a half-finished sign-up: the GoTrue login exists but the account row does not --- logins 1 accounts 0 --- logging in repairs it, and claims the install because nobody holds Owner --- $ curl -s -X POST $BASE/api/auth/login { "account": { "id": "157b344d-...", "email": "owner@example.com", "isOwner": true } } HTTP 200 email | is_owner -------------------+---------- owner@example.com | t

--- a half-finished sign-up: the GoTrue login exists but the account row does not ---
$ psql -c "delete from account"      # simulate the account write never landing
DELETE 1
$ psql -c "select count(*) as logins from auth.users" -c "select count(*) as accounts from account"
 logins 
--------
      1
(1 row)

 accounts 
----------
        0
(1 row)


--- logging in repairs it, and claims the install because nobody holds Owner ---
$ curl -s -X POST $BASE/api/auth/login -d {"email":"owner@example.com","password":"..."}
{
  "account": {
    "id": "157b344d-83b3-4fdf-b3ce-cd375a7492b2",
    "email": "owner@example.com",
    "isOwner": true
  }
}
HTTP 200

$ psql -c "select email, is_owner from account"
       email       | is_owner 
-------------------+----------
 owner@example.com | t
(1 row)
Evidence: Vitest verbose run (5 files, 34 tests)

 RUN  v4.1.11 .

 ✓ tests/test-job.spec.ts > runTestJob > carries the note and the send time through and stamps when it ran 25ms
 ✓ tests/queue.spec.ts > the queue module > starts once and reuses the same connection 355ms
 ✓ tests/queue.spec.ts > the queue module > stops cleanly after a start that failed, instead of throwing a second time 13ms
 ✓ tests/queue.spec.ts > the queue module > stops cleanly when the shutdown lands while a failing start is still in flight 8ms
 ✓ tests/queue.spec.ts > the queue module > closes the connection pool when the start fails after it has connected 3ms
 ✓ tests/queue.spec.ts > the queue module > retries the start after a failure rather than caching the rejection 14ms
 ✓ tests/queue.spec.ts > the queue module > does not let a failing stop reject on the shutdown path 3ms
 ✓ tests/storage.spec.ts > the filesystem driver > round-trips bytes under a nested key 71ms
 ✓ tests/storage.spec.ts > the filesystem driver > reports a missing key as absent and deletes without complaint 20ms
 ✓ tests/storage.spec.ts > the filesystem driver > does not report the directory a key sits in as a stored object 31ms
 ✓ tests/storage.spec.ts > the filesystem driver > reports a failure that is not a missing key rather than answering false 15ms
 ✓ tests/storage.spec.ts > the filesystem driver > refuses a key that would climb out of the root 6ms
 ✓ tests/storage.spec.ts > key validation > rejects "" 2ms
 ✓ tests/storage.spec.ts > key validation > rejects "/absolute" 0ms
 ✓ tests/storage.spec.ts > key validation > rejects "up/../out" 0ms
 ✓ tests/storage.spec.ts > key validation > rejects "back\\slash" 1ms
 ✓ tests/storage.spec.ts > key validation > rejects "double//slash" 1ms
 ✓ tests/storage.spec.ts > key validation > rejects "trailing/" 1ms
 ✓ tests/storage.spec.ts > key validation > accepts an ordinary relative key 1ms
 ✓ tests/storage.spec.ts > the s3 driver > is a declared switch that is not implemented yet 1ms
 ✓ tests/gotrue.spec.ts > authUserIdFromToken > accepts a login token and returns the auth.users id 67ms
 ✓ tests/gotrue.spec.ts > authUserIdFromToken > refuses the service key, which shares the same signing secret 29ms
 ✓ tests/gotrue.spec.ts > authUserIdFromToken > refuses a token minted for another audience 32ms
 ✓ tests/account.spec.ts > normaliseLoginEmail > folds case and trims, so one person cannot hold two Accounts 6ms
 ✓ tests/account.spec.ts > normaliseLoginEmail > leaves an already normal address alone 1ms
 ✓ tests/account.spec.ts > isOwnerAlreadyTaken > reads through the wrapper drizzle puts around the error Postgres raised 1ms
 ✓ tests/account.spec.ts > isOwnerAlreadyTaken > is false for a duplicate on some other index 1ms
 ✓ tests/account.spec.ts > isOwnerAlreadyTaken > is false for an error that is not a duplicate at all 1ms
 ✓ tests/account.spec.ts > findOrCreateAccountForLogin > returns the row that is already there without writing 3ms
 ✓ tests/account.spec.ts > findOrCreateAccountForLogin > claims the install when the sign-up that should have written the Account did not 4ms
 ✓ tests/account.spec.ts > findOrCreateAccountForLogin > returns the winner row when another repair wrote the Account first 1ms
 ✓ tests/account.spec.ts > findOrCreateAccountForLogin > retries as a non-Owner when another repair claimed the install first 4ms
 ✓ tests/account.spec.ts > findOrCreateAccountForLogin > gives up rather than looping when the duplicate never resolves 5ms
 ✓ tests/account.spec.ts > findOrCreateAccountForLogin > lets an error that is not a duplicate travel 1ms

 Test Files  5 passed (5)
      Tests  34 passed (34)
   Start at  10:39:46
   Duration  3.86s (transform 2.32s, setup 0ms, import 6.02s, tests 827ms, environment 2ms)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ server/api/auth/signup.post.ts:17 - isOwnerAlreadyTaken() reads code and constraint off the error it is handed, but drizzle-orm 0.45.2 (pinned in pnpm-lock.yaml) wraps every failed query in DrizzleQueryError, which carries only query, params and cause and never copies the pg fields. Verified against the published drizzle-orm@0.45.2 tarball: pg-core/session.js throws new DrizzleQueryError(queryString, params, e) on every execute path, and errors.js shows the constructor sets no code/constraint. So the guard is always false and the intended 403 is unreachable. Fix at the shared boundary rather than in this one route: add a helper in core/account.ts (the single owner of account writes) that unwraps the DrizzleQueryError cause chain and reports whether a given unique constraint was violated, then use it here and in login.post.ts:26.
  • ⚠️ server/api/auth/login.post.ts:26 - The login repair path calls createAccount() with no handling for a unique violation, so any concurrent repair surfaces as an unhandled 500. Concrete path A: one GoTrue login exists with no account row (a sign-up whose account write and whose rollback both failed); the person logs in from two tabs at once; both reach line 24 with no row, both insert, and the loser violates account_auth_user_id_unique (23505) and gets a 500 even though a retry would succeed. Concrete path B: two orphaned GoTrue logins on an install with no Owner log in at the same moment; both evaluate installHasOwner() as false, both insert with isOwner true, and the loser violates account_one_owner and gets a 500 instead of being created as a non-Owner. The data invariant holds (the partial index does its job), but the answer is wrong. Use the same core/account.ts unwrap helper proposed for signup.post.ts: on an auth_user_id conflict re-read and return the existing row, on an owner conflict retry the insert with isOwner false.
  • ℹ️ seed/index.ts:40 - createGoTrueUser() succeeds, then createAccount() throws (transient database error, or the account_one_owner index losing a race with a browser sign-up). Unlike signup.post.ts, seed does not delete the GoTrue login it just made, so the install is left with a login and no Account. The next pnpm seed then hits the 422 branch at line 34, whose message tells the operator to drop the volume - which contradicts the repair story the rest of the change documents, where logging in recreates the missing Account row. Either mirror the signup rollback here, or point the 422 message at the login repair path instead of at dropping the volume.

🔧 Fix: Unwrap drizzle errors, handle login repair races, roll back seed
2 infos still open:

  • ℹ️ seed/index.ts:48 - When the seed loses the Owner race it rethrows the raw DrizzleQueryError instead of the refusal it already has words for. Concrete path: the install is unclaimed, so line 29 passes; a browser sign-up claims it while the seed is still calling GoTrue; the insert at line 44 loses on the account_one_owner partial index. The data is correct, but the operator sees 'Failed query: insert into "account" (...) params: <uuid>,owner@example.com,true,...' rather than the message line 30 prints for the very same refusal. isOwnerAlreadyTaken() is exported from core/account.ts now, so the fix is one branch in the existing catch: if isOwnerAlreadyTaken(error), throw the same 'This install has an Owner already' Error line 30 uses.
  • ℹ️ core/account.ts:37 - installHasOwner() lost its only external caller when login.post.ts moved to findOrCreateAccountForLogin(). Grep over *.ts and *.vue shows the sole remaining use is core/account.ts:97, inside the same module. Dropping the export keeps the module's public surface equal to what callers actually reach for; no behavior changes.
✅ **Test** - passed

✅ No issues found.

  • docker compose -f docker-compose.dev.yml up -d then docker compose -f docker-compose.dev.yml ps - both services healthy, ports bound to loopback
  • docker compose -f docker-compose.dev.yml config --services -> postgres, gotrue only; docker compose -f docker-compose.yml config --services -> all five (postgres, gotrue, web, worker, caddy)
  • curl -s http://127.0.0.1:9999/health - GoTrue answering
  • pnpm db:migrate and pnpm db:check-auth against the fresh volume
  • Manual browser run of pnpm dev (port 3300, 3000 was taken): claimed the install as a fabricated Owner, signed out, signed back in; screenshots at each step
  • pnpm worker in one shell + pnpm queue:ping &#34;a fabricated test job from the scaffold check&#34; in another; job id matched on both sides and pgboss.job shows state=completed
  • pnpm lint with a deliberate core/deliberate-violation.ts importing ../worker/index - failed with the ADR 0004 no-restricted-imports error, exit 1; file removed afterwards
  • pnpm test / pnpm vitest run --reporter=verbose - 5 files, 34 tests
  • E2E_PORT=3300 pnpm test:e2e --reporter=list - 1 test
  • pnpm seed against a claimed install (refused, exit 1) and against a reset install (created fabricated Owner owner@example.com)
  • psql -c &#34;insert into account (..., is_owner) values (..., true)&#34; on a claimed install - rejected by unique index account_one_owner; the same insert with is_owner=false succeeded
  • curl transcript over the real API: /api/auth/status, /api/auth/signup (403 once claimed), /api/auth/login, /api/auth/me, /api/auth/logout
  • Login repair path: deleted the account row leaving the GoTrue login, then POST /api/auth/login recreated the Account and claimed Owner
  • Storage module driven through the public storage() selector in two processes: STORAGE_DRIVER=filesystem round-tripped bytes to .data/storage, STORAGE_DRIVER=s3 refused with the not-implemented error
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

First code in the repo. No domain logic, no AI call, no production Dockerfile,
no Caddy: only the skeleton every later ticket needs somewhere to put code in.

- One package at the root with app/, server/, core/, db/, queue/, worker/,
  seed/, storage/, tests/ and e2e/. server/ and worker/ both import core/, and
  an ESLint rule fails the build if core/ ever imports either.
- docker-compose.dev.yml runs Postgres and GoTrue and nothing else.
  docker-compose.yml names all five services and references a Dockerfile and a
  Caddyfile that belong to the packaging and publishing tickets.
- .env.example names every variable and holds no real value.
- Drizzle wired to Postgres with a first migration for the account table, plus
  GoTrue's auth schema declared read-only and pnpm db:check-auth to prove we can
  read what we did not create.
- pg-boss behind a single queue/ module, with worker/ as its consumer.
- storage/ with a filesystem driver and a declared S3 switch that refuses.
- Vitest and Playwright, and a GitHub Actions workflow that runs lint,
  typecheck, both suites and a production build.

Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
…text, exists()

Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
…L-safe passwords

Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
…chema and seed

Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
…d issuer

Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
Signed-off-by: PP <121104417+BohnBawerick@users.noreply.github.com>
@BohnBawerick
BohnBawerick merged commit 4787c80 into main Aug 24, 2026
1 check passed
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