00 - Bootstrap
On this page 5
Getting the application to exist, run, and talk to a database. Complete.
Several items here were framework or tooling bugs rather than application work. They are recorded because the fixes live in other repositories, and because anyone reproducing this setup on a clean machine benefits from knowing what was wrong.
Application
- Scaffold with
buddy newinto the existing repository - Package-based layout: no
storage/framework/core, every@stacksjs/*resolved from npm - Generate
APP_KEYand drop the template's undecryptable encrypted env files - Name the application:
package.json,config/app.ts, README, MIT license - Point
lint,typecheck, andtestscripts at the buddy commands - Disable the commerce, cms, marketing, and monitoring feature bundles
- Write
AGENTS.md: domain vocabulary, git storage layout, framework sync-back rule ./buddy setup:ai claudefor the skill set and launch config
Database
- Switch
DB_CONNECTIONto postgres in.envand.env.example DB_USERNAME=postgres, the only role pantry's cluster has- Regenerate the migration corpus for Postgres (
./buddy migrate:regenerate postgres) - Remove the stale SQLite model snapshot so the dialect guard passes
./buddy migrateapplies cleanly: 82 tables./buddy seedruns without errors- Database is created automatically from
.envby pantry, not by hand
Environment
deps.yamlgenerated fromconfig/deps.tsplus.env, installing Postgres 17 and Bun 1.3.14- PostgreSQL starts as a pantry service before anything tries to connect
- Link the local Stacks checkout with
./buddy link:core --all
Upstream fixes this required
Each one is committed and pushed in the repository named.
-
stacks -
buddy newrefused any existing directory, so cloning a repository first and scaffolding into it was impossible. It now accepts an empty directory, or one holding only.git, and skipsgit initwhen a repository is already there. -
stacks -
buddy newnow resolves the framework from npm by default rather than vendoring 2,000 files into the first commit.--with-coreopts back in. -
stacks -
buddy setupinstalled every database engine rather than the oneDB_CONNECTIONnames, and emitted no services section, so PostgreSQL was installed but never started and its own database creation failed with a connection refused. -
stacks - The query log columns hold whole SQL statements and stack traces but were
varchar(255). SQLite never minded; Postgres rejected every insert. -
stacks - Query logging recorded the literal string
[object Promise]for every statement, which also made the N+1 detector report[OBJECT PROMISE]as the repeating query shape. -
stacks - A 13 MB packed tarball was committed at the repository root and shipped into every scaffolded project.
-
pantry - Two of the four
initdbcall sites omitted--username=postgres, so whichever one created the cluster decided its superuser. Database creation then failed withrole "postgres" does not existagainst pantry's own cluster. -
pantry - Service units are per-project but the PostgreSQL data directory was global, so two projects on different majors destroyed each other's cluster in a loop, each backing up and re-initializing what the other had just built.
-
pickier - A function whose return type is written as an inline union (
): { ok: true, ... } | { ok: false, ... } {) makesno-unused-varsreport every parameter as unused, because the parser does not find the body.transitionDraftinapp/Actions/Pull/state.tsis the case that found it, andresolveExpiryinapp/TokenScopes.tsis the case that proved it recurs.Fixed upstream and verified against the published build: the rule scanned past the return type for a
{and stopped at the first one following a completed brace pair, so the second member of the union was read as the body. The body then read as empty and--fixrenamed every parameter to_namewhile the body kept referring toname- code that no longer compiles, which is what makes it worse than noise.pickier@0.1.49, withtest/rules/no-unused-vars-return-types.test.tspinning it. -
pickier -
no-unused-varsalso missed a module-levelconstreferenced before it is declared, which is ordinary and valid: the eight content constants inSeedDemo.tswere each reported as unused while being used. Also fixed in0.1.49.Both workarounds stay. Naming the union and moving the constants into
demo-content.tswere better code independently of the linter, and reverting structure to prove a tool is fixed is how you end up doing it twice. -
bun-query-builder - Enum type names are table-qualified, but only newly added columns were stamped with the qualified name, so altering an existing enum column referenced a type nothing creates. Migrating to Postgres died on the last file with
type "channel_type" does not exist. -
bun-query-builder - The seeder handed factories
@stacksjs/ts-fakerdirectly, and every factory in this ecosystem is written in the faker-js dialect:helpers.arrayElementthrew,string.alphanumeric(12)silently returned one character,datatypeandlocationdid not exist. The compat layer now translates both ways and is exported as a type, so a factory written the normal way is neither a runtime error nor a type error.0.2.29. -
ts-validation - The declaration emitter widened
EnumValidator.nametounknown, soschema.enum([...])was not assignable to theEnumValidatorTypethis library exports and a framework's env config rejected values it validates happily at runtime. A.d.tsbug wearing a type error's clothes.0.5.4, with a type test. -
stacks -
@stacksjs/fakerbuilds an enhanced faker -datatype,location,helpers.arrayElement,catchPhrase- and exported the type of the library underneath it. Every model factory in every Stacks application was therefore a type error against a type describing a different object: 116 of them here, 73 in the framework's own default models. Fixed in 0.70.371 along with the env config's enum type. -
stacks - bunpress was pinned at
^0.1.18, where/search-index.jsondoes not exist, so every documentation site built on this framework had a search box that took a query and answered nothing.0.70.370. -
stacks -
faker.datatype.boolean(0.2)- the bare-probability form faker-js accepts, and the form the framework's ownProductUnitmodel uses - was a type error, and the one error./buddy typecheckreported here. It was fixed and tagged as 0.70.372, and that release could not publish: theReleaserjob failed buildingstorage/framework/core/mobile, which importscraft-native/mobile- a subpath the publishedcraft-native@0.0.55did not export.**Closed by the 0.72 line, which carries the fix and publishes.** The probability form typechecks against the installed framework, and `app/Models/ReviewThread.ts` uses it rather than a bare `boolean()`: a factory that resolves a fifth of its threads is closer to a real repository than one that resolves half, and a use in the tree is what makes a regression here fail `./buddy typecheck` instead of going unnoticed until somebody writes the form again. -
bun-router - Static files were served gzipped and nothing else was. Every response a route produced went out whole: 253 KB of HTML on this project's landing page,
Accept-Encodingignored, on every request from every visitor. Compression now happens once, where every response passes through, with the two rules that make it safe rather than merely smaller - aVaryon both branches, so a cache cannot hand a gzipped body to a client that did not ask, and the body piped rather than buffered, so a streamed response keeps streaming: this product's diff manifest still arrives a file at a time, now compressed.The first attempt used `Content-Length` to tell a buffered response from a stream, and Bun does not set one on a `Response` built from a string - so the guard skipped every server-rendered page, which is the entire case. Piping needs no length and covers both. Two follow-ups, both found by watching what the application served rather than by reading the code: with no length there was nothing to compare against the threshold, so a 356-byte `/api/health` was being gzipped into something larger - the body is now peeked up to the threshold and no further - and `text/event-stream` matched `text/*` and was eligible, which would have turned a live channel into one that arrives in clumps. `0.0.26`. Measured on the landing page: **252,661 bytes to 61,184 on the wire**, with `/api/health` untouched and the diff manifest still arriving a file at a time.
Known gaps, deferred deliberately
-
Stacks -
notifications.user_idandnotification_deliveries.user_idforeign keys were missing from the live schema on every installation, not just this one.The last note here had the reproduction right and the mechanism half right, and guessed at the ordering. It is not a guarantee running during boot:
runDatabaseMigrationcallsmigrateNotificationTables()before the model batch, deliberately and with a comment saying why - a generated model migration may normalize or rebuild these tables and needs them to exist first. That is also exactly why the keys never landed. The guarantee creates the table without them, and the model's ownCREATE TABLE IF NOT EXISTS … REFERENCES "users"("id") ON DELETE CASCADEis then a no-op against a table that already exists. The migration runs, the corpus declares the key, and the key is not there.Putting
REFERENCESinline in the guarantee does not work either: on a brand-new database nothing has createdusersyet, so the CREATE would fail and take the boot with it.Fixed in Stacks 0.70.318 by adding the keys after the batch, when
usersis certain to exist- the same defensive-ALTER-and-swallow pattern
ensureUsersAuthColumnsalready uses inauth-tables.ts, and for the same reason: an installation that has deliberately dropped the relation must not fail its migration over a constraint it does not want. Verified against this database, which now carries both with the cascade.
- the same defensive-ALTER-and-swallow pattern
-
The
jobstable was dropped bymigrate:freshand not recreated by the corpus, so seeding skipped it.Resolved by the thing that fixed the queue itself:
app/Models/Job.tsoverrides the framework default, because two of its columns describe something other than what@stacksjs/queuestores. A model in the corpus means a generated migration in the corpus, so0000000012-create-jobs-table.sqlis replayed bymigrate:freshlike anything else - the table had been missing precisely because nothing described it here. -
Pantry could not auto-activate environments under the
denshell: it recognized zsh, bash, fish and nushell, and den has nochpwdor pre-prompt hook to attach to.Fixed upstream and verified against the installed binary -
PANTRY_SHELL=den pantry dev:shellcodeemits it. The hook is written out and sourced rather than eval'd, because den'sevalparses its argument as a command chain and a chain cannot carry a function definition, so an eval'd hook defines nothing. It also looks nothing like the bash template on purpose: in den a[test costs around 5ms where bash measures it in microseconds, so the shell-side parent walk that other shells use would spend most of a second percddeciding a directory is not a project. It forkspantry shell:lookuponce instead and lets native code walk.