What happened
I was setting up local development to try Facility on a real repository. pnpm dev seeds a demo org, user and membership, and nothing in the local flow ever writes the two rows a GitHub App needs — the identity and the installation. So after creating the App, setting all four GITHUB_APP_* values and installing it, project creation still told me no installation was visible.
The App itself was fine: I called GET /app and GET /app/installations myself, both returned 200 and the installation lists my repository. The advice in that message — install the App and reload, or type owner/name — can't work either, because the list is read from the database rather than from GitHub.
The only writer of github_installations in the repository is facility instance bootstrap, which the self-host docs describe, so I ran that. It refused with Database already contains a different Facility instance — because of the demo seed, which is the part I did not expect. I got unblocked by inserting the row by hand; my instance still has zero identity rows and is still on the demo org.
How to reproduce
Two commands on a clean database: the seed exactly as pnpm dev runs it, then bootstrap
FACILITY_SEED_DEMO=1 node packages/db/dist/bin/seed.js
node packages/cli/bin/facility.mjs instance bootstrap \
--org-name Acme --org-slug acme --owner-email dev@example.com --owner-name Dev \
--github-user-id 1 --github-login dev --github-account-id 1 \
--github-account-login dev --github-installation-id 999 --github-account-type user
Result: Database already contains a different Facility instance
How are you running Facility?
Local development (pnpm dev)
Version
main at 11df0f9
Evidence
Nothing in the local flow writes the installation. seedLocalDemo in packages/db/src/seed.ts:49-73 writes the org, the user and the membership and stops there, and scripts/dev.mjs never mentions GitHub at all. After the demo seed the database holds orgs=1 users=1 members=1 identities=0 installations=0 — three of the five rows a working GitHub setup needs.
Nothing else fills the gap. routes/v1/github-installations.ts:50-58 lists from the database and never asks GitHub, so installing the App and reloading changes nothing. outes/webhooks.ts:61 drops deliveries from unknown installations, so installation.created can't self-register. routes/v1/projects.ts:284-290 requires the same row, so the type owner/name fallback fails with github_installation_required.
And the one command that can write it refuses. At packages/cli/src/instance.mjs:41, hasInstanceData is true if any of those five counts is non-zero — the demo seed makes three of them non-zero. The query that then decides whether the existing instance matches mine inner-joins user_identities and github_installations, both empty, so it returns no rows, identical is false, and it throws at line 69. The guard is meant to catch a conflicting instance; a demo-seeded org isn't one, because it has no binding to conflict with. It is incomplete, not conflicting — but the result is that the state can neither be adopted nor repaired.
The same check is strict beyond this case. exactlyOneBinding requires every count to equal 1, so re-running bootstrap with the identical values it was first given fails on any instance that has grown past one user or one installation. The command otherwise implements idempotency — it returns created: false and prints "already bootstrapped" — and #336 would make re-running routine via a Compose profile.
Related: #336 also touches instance.mjs, but only how bootstrap reads its input; it leaves this guard unchanged.
What happened
I was setting up local development to try Facility on a real repository.
pnpm devseeds a demo org, user and membership, and nothing in the local flow ever writes the two rows a GitHub App needs — the identity and the installation. So after creating the App, setting all fourGITHUB_APP_*values and installing it, project creation still told me no installation was visible.The App itself was fine: I called
GET /appandGET /app/installationsmyself, both returned 200 and the installation lists my repository. The advice in that message — install the App and reload, or type owner/name — can't work either, because the list is read from the database rather than from GitHub.The only writer of
github_installationsin the repository is facility instance bootstrap, which the self-host docs describe, so I ran that. It refused with Database already contains a different Facility instance — because of the demo seed, which is the part I did not expect. I got unblocked by inserting the row by hand; my instance still has zero identity rows and is still on the demo org.How to reproduce
Two commands on a clean database: the seed exactly as
pnpm devruns it, thenbootstrapResult: Database already contains a different Facility instance
How are you running Facility?
Local development (pnpm dev)
Version
main at 11df0f9
Evidence
Nothing in the local flow writes the installation. seedLocalDemo in packages/db/src/seed.ts:49-73 writes the org, the user and the membership and stops there, and scripts/dev.mjs never mentions GitHub at all. After the demo seed the database holds orgs=1 users=1 members=1 identities=0 installations=0 — three of the five rows a working GitHub setup needs.
Nothing else fills the gap.
routes/v1/github-installations.ts:50-58lists from the database and never asks GitHub, so installing the App and reloading changes nothing.outes/webhooks.ts:61drops deliveries from unknown installations, so installation.created can't self-register.routes/v1/projects.ts:284-290requires the same row, so the type owner/name fallback fails withgithub_installation_required.And the one command that can write it refuses. At
packages/cli/src/instance.mjs:41,hasInstanceDataistrueif any of those five counts is non-zero — the demo seed makes three of them non-zero. The query that then decides whether the existing instance matches mine inner-joinsuser_identitiesandgithub_installations, both empty, so it returns no rows, identical is false, and it throws at line 69. The guard is meant to catch a conflicting instance; a demo-seeded org isn't one, because it has no binding to conflict with. It is incomplete, not conflicting — but the result is that the state can neither be adopted nor repaired.The same check is strict beyond this case.
exactlyOneBindingrequires every count to equal 1, so re-running bootstrap with the identical values it was first given fails on any instance that has grown past one user or one installation. The command otherwise implements idempotency — it returns created: false and prints "already bootstrapped" — and #336 would make re-running routine via a Compose profile.Related: #336 also touches instance.mjs, but only how bootstrap reads its input; it leaves this guard unchanged.