Twice now we've shipped a deploy that 500'd in production because
drizzle silently skipped freshly-generated migrations whose `when`
timestamps were older than a prior manually-bumped entry (0010/0011
in 1b7f553, then 0012/0013 in 2731888). Both times pnpm migrate
printed "Migrations applied." while the live DB schema lagged the
code's expectations.
Three layers of defence:
1. packages/db/src/journal-check.ts — pure helpers
- assertJournalMonotonic(entries): walks idx-sorted entries and
returns each one whose `when` <= the previous entry's `when`,
plus a suggested `when` value to bump it to.
- formatJournalViolations(result): renders an actionable
multi-line message that points at the offending file path.
2. packages/db/src/migrate.ts — pre-flight
Reads _journal.json BEFORE handing it to drizzle.migrate(). If
the journal is non-monotonic, it prints the violations + bump
instructions and exits with code 2. No more "Migrations applied."
while silently skipping.
3. apps/web/src/test/drizzle-journal-monotonic.test.ts — CI guard
Reads the committed _journal.json at test time. CI fails on the
PR before the bad commit can ship. Imports the helper through a
new "./journal-check" subpath export on @cmbot/db so the test
doesn't rely on a deep path into the package.
Together: a bad commit fails CI; if it somehow got through, migrate
itself refuses to run; if migrate is bypassed, the previous deploy's
schema stays intact (drizzle wouldn't have skipped anything in any
case where the journal is monotonic).
Web suite 480 → 482 tests, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-JS bcrypt for password hashing. Avoids the native-build pain
of node-bcrypt in our Alpine Docker images. Login is a rare event
so the perf gap is irrelevant for our scale.
Two related fixes:
1. Phone (and any LAN client) couldn't reach the web container because
the dev compose mapped 127.0.0.1:WEB_PORT instead of binding all
interfaces. Drop the loopback prefix.
2. Turbopack and NodeNext disagree on extension handling: bot's tsc
needs `.js` extensions in source imports; Turbopack's transpilePackages
path can't resolve those `.js` requests back to `.ts` source. Switch
to consuming the workspace packages via their compiled dist instead:
- packages/db + packages/shared point `main`/`exports` at ./dist/*
- drop transpilePackages from next.config.ts; web picks up the
compiled `.js` files directly
- dev compose command for web builds shared+db before running
`next dev` so dist is fresh when Turbopack starts
- put the `.js` extensions back in packages/db source so NodeNext
compilers (bot's tsc, packages/db's own tsc) are happy
Three small build-time fixes surfaced when the Docker images first ran
their full production build (previously only dev mode via tsx):
- packages/shared: exclude *.test.ts from tsc (vitest types not needed
for shipped output), add @types/node dep so node:crypto resolves
- packages/db: add @types/node dep for the same reason
- apps/web: pin Next.js Turbopack root to the workspace root via
next.config.ts so the bundler doesn't fail to detect the monorepo
layout from inside the Docker image