A server died on `Cannot find module '../../../../plugins/music/db/schema'`
before a line of its own code ran. The pull that broke it was the one untracking
`plugins/` — git deleted those directories from the working tree, and the
gitignored `plugin-schemas.gen.ts` still named them.
The barrel being wrong is NORMAL. It is written by `db:push` and by installing a
plugin, and nothing rewrites it when a directory goes away by other means: a
pull, `git clean -xfd`, `rm`. It describes a machine, not the project, which is
why it is gitignored in the first place.
The defect was not the stale file. It was that a file allowed to be wrong sat in
the import graph of the process that must always start — via `db.ts` →
`schema.ts` → the barrel. An import graph is resolved before `main`, so this
was not catchable, loggable or survivable. Everywhere else the plugin system
already holds the line: `refreshPluginMounts` isolates a bad plugin,
`discoverPlugins` reports a broken manifest instead of throwing. This was the one
remaining path that could take the whole server down, and it took it down.
So the two consumers get their own entry points. `schema.ts` keeps the core
tables and is what the runtime imports. `drizzle-schema.ts` re-exports it, adds
the generated barrel, and is read only by drizzle-kit — which regenerates the
barrel before every run and can afford to fail loudly on a genuinely broken
plugin. `drizzle.config.ts` points at the new one.
Verified by reproducing it: with the barrel naming a plugin directory that does
not exist, importing the server's db module now succeeds while importing
`drizzle-schema.ts` still fails with that exact error — so the runtime is immune
and drizzle-kit's strictness is intact. First attempt at that test was worthless,
since I "poisoned" the barrel with a path that existed here; redone with an
absent one. `bun db:gen` then loaded the new entry and emitted both core and
plugin tables (music_*, offscale_servers), which is what proves `db:push` still
works.
Pulling this is enough to boot a server in that state — the stale barrel stops
being loaded rather than needing to be repaired.
tsgo clean, frontend builds, 865 pass / 7 fail unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`officer_db/src/schema.ts` ends with an UNCONDITIONAL
`export * from './plugin-schemas.gen'`. That file is gitignored — it describes
this machine, not the project — and until now its only writer was
installPlugin.
So on a fresh clone the file does not exist and `bun db:push` dies with
MODULE_NOT_FOUND before creating a single table. That is exactly what
officer-setup.sh section 8 runs, so setup failed at the schema step on any
clean machine, and the only thing that would have written the file was
installing a plugin — which needs a working database. A deadlock, shipped with
the plugin system on 2026-08-15.
Reproduced by deleting the file and running push, not inferred.
`db:push` now runs scripts/gen-plugin-schemas.ts first. Wired into the script
rather than added as a setup step because setup is not the only caller:
pushSchema() shells out to the same command, a developer types it by hand, and
`git clean -xfd` removes the file at any time. A step someone has to remember
is one they forget once; push regenerating it means the barrel cannot be stale
when drizzle reads it.
Both paths verified: barrel deleted then push (regenerates, "No changes
detected"), and zero plugins on disk (writes a header-only barrel that
schema.ts still imports cleanly).
Also verified, on a scratch database, the premise the whole barrel design rests
on — that push drops what it cannot see. Full schema pushed, a row seeded into
music_favorites, then an empty barrel pushed: music_* and headscale_servers
were DROPPED along with the row. So "following the directory rather than the
install table" is load-bearing, not decorative.
Worth recording how close that came to being logged as the opposite. An earlier
run with an empty barrel reported "No changes detected", which read as evidence
that push does not drop. It was this fix working — the script had regenerated
the barrel before drizzle ran. The scratch database is what told them apart.
bunx tsgo clean. 797 tests, 787 pass, same 7 pre-existing failures. Scratch
database dropped; live plugin tables and rows verified intact.