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 new into the existing repository
  • Package-based layout: no storage/framework/core, every @stacksjs/* resolved from npm
  • Generate APP_KEY and drop the template's undecryptable encrypted env files
  • Name the application: package.json, config/app.ts, README, MIT license
  • Point lint, typecheck, and test scripts 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 claude for the skill set and launch config

Database

  • Switch DB_CONNECTION to postgres in .env and .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 migrate applies cleanly: 82 tables
  • ./buddy seed runs without errors
  • Database is created automatically from .env by pantry, not by hand

Environment

  • deps.yaml generated from config/deps.ts plus .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 new refused 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 skips git init when a repository is already there.

  • stacks - buddy new now resolves the framework from npm by default rather than vendoring 2,000 files into the first commit. --with-core opts back in.

  • stacks - buddy setup installed every database engine rather than the one DB_CONNECTION names, 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 initdb call sites omitted --username=postgres, so whichever one created the cluster decided its superuser. Database creation then failed with role "postgres" does not exist against 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, ... } {) makes no-unused-vars report every parameter as unused, because the parser does not find the body. transitionDraft in app/Actions/Pull/state.ts is the case that found it, and resolveExpiry in app/TokenScopes.ts is 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 --fix renamed every parameter to _name while the body kept referring to name - code that no longer compiles, which is what makes it worse than noise. pickier@0.1.49, with test/rules/no-unused-vars-return-types.test.ts pinning it.

  • pickier - no-unused-vars also missed a module-level const referenced before it is declared, which is ordinary and valid: the eight content constants in SeedDemo.ts were each reported as unused while being used. Also fixed in 0.1.49.

    Both workarounds stay. Naming the union and moving the constants into demo-content.ts were 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-faker directly, and every factory in this ecosystem is written in the faker-js dialect: helpers.arrayElement threw, string.alphanumeric(12) silently returned one character, datatype and location did 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.name to unknown, so schema.enum([...]) was not assignable to the EnumValidatorType this library exports and a framework's env config rejected values it validates happily at runtime. A .d.ts bug wearing a type error's clothes. 0.5.4, with a type test.

  • stacks - @stacksjs/faker builds 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.json does 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 own ProductUnit model uses - was a type error, and the one error ./buddy typecheck reported here. It was fixed and tagged as 0.70.372, and that release could not publish: the Releaser job failed building storage/framework/core/mobile, which imports craft-native/mobile - a subpath the published craft-native@0.0.55 did 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-Encoding ignored, 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 - a Vary on 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_id and notification_deliveries.user_id foreign 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: runDatabaseMigration calls migrateNotificationTables() 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 own CREATE TABLE IF NOT EXISTS … REFERENCES "users"("id") ON DELETE CASCADE is 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 REFERENCES inline in the guarantee does not work either: on a brand-new database nothing has created users yet, 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 users is certain to exist

    • the same defensive-ALTER-and-swallow pattern ensureUsersAuthColumns already uses in auth-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 jobs table was dropped by migrate:fresh and not recreated by the corpus, so seeding skipped it.

    Resolved by the thing that fixed the queue itself: app/Models/Job.ts overrides the framework default, because two of its columns describe something other than what @stacksjs/queue stores. A model in the corpus means a generated migration in the corpus, so 0000000012-create-jobs-table.sql is replayed by migrate:fresh like anything else - the table had been missing precisely because nothing described it here.

  • Pantry could not auto-activate environments under the den shell: it recognized zsh, bash, fish and nushell, and den has no chpwd or pre-prompt hook to attach to.

    Fixed upstream and verified against the installed binary - PANTRY_SHELL=den pantry dev:shellcode emits it. The hook is written out and sourced rather than eval'd, because den's eval parses 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 per cd deciding a directory is not a project. It forks pantry shell:lookup once instead and lets native code walk.