Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion STACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ classes. George's standing decision (2026-09-01): uniform on the table below.
| Validation | zod | — |
| ORM / DB access | **Drizzle ORM** + `pg` driver | orangecat, botsmann: supabase-js (self-hosted Supabase architecture: RLS/auth/PostgREST). fleetcrown runner & ivy-portal: better-sqlite3 for embedded local state. |
| Database | Postgres (self-hosted; Supabase where the app is Supabase-native) | fleetcrown runner/ivy: SQLite embedded |
| Auth | next-auth (v5 when stable) | orangecat, botsmann: Supabase Auth (architecture, not drift) |
| Auth — **who the user is** | **Federate to OrangeCat** (OIDC, `openid profile email`). The app keeps NO users table. See "Identity" below. | orangecat itself IS the identity provider. Client-owned apps never federate — their users belong to the client. |
| Auth — **client-owned apps** | **`better-auth` 1.x** + Drizzle/Postgres, magic link via `@bitbaum/mail-kit` | aoz-housing (hand-rolled `jose`), botsmann + printcraft (Supabase Auth) — all pre-date the decision; migrate on contact, not on a schedule |
| Test runner (apps) | Vitest | fleetcrown: bespoke tsx gate scripts (deliberate architecture — each script is a named gate) |
| Test runner (packages) | node:test (zero-dep) | — |
| E2E | Playwright | — |
Expand All @@ -32,6 +33,65 @@ classes. George's standing decision (2026-09-01): uniform on the table below.
| Runtime | Node LTS (currently 24), nodesource on the box | openclaw gateway: its own nvm-pinned Node |
| Deploy | push → PR → CI → auto-merge sweep → CD → box (systemd + Caddy) | — |

## Identity (2026-09-11)

**There are two populations of user, and conflating them is the mistake this
section exists to prevent.**

**1. Our own products federate.** OrangeCat is the identity SSOT and has been
since its OIDC provider shipped 2026-06-17 — discovery, authorize, token,
userinfo, jwks, PKCE, refresh rotation. FleetCrown federated 2026-07-02, Solon
after, Heidi 2026-09-11. A federated app keeps **no users table, no password,
no reset flow, no session table**. It reads `id_token.sub` — the actor id,
never the email — and stops.

The instruction the identity-bridge spec gives FleetCrown generalises to all of
them: *"Do NOT build profiles, walls, or messaging inside FleetCrown."* If your
app needs profiles, payments or a public presence, those live at OrangeCat.
Rebuilding them locally is the same mistake in a different repo.

Register a client by adding a spec to orangecat's
`scripts/oauth/register-client.ts` — identity scopes only unless the app
genuinely acts on OrangeCat's behalf — then run it on the box. Copy Solon's or
Heidi's provider config verbatim: OrangeCat's token endpoint accepts ONLY
`client_secret_post` (Auth.js defaults to `client_secret_basic`, which OC
rejects with a 400 reading "client_id is required"), and PKCE is required even
for confidential clients. Both cost a debugging cycle the first time.

**2. Client-owned apps never federate.** aoz-housing's residents belong to AOZ,
not to us. Those apps keep local auth, and the blessed library for new local
auth is **better-auth 1.x**.

### Why better-auth and not next-auth, for the local case

Because `next-auth` could not be enforced, and this file's whole premise is
that unenforced choices drift. `blessed-versions.json` says so in its own
comment — *"next-auth is deliberately absent while v5 is beta"* — so
`version-currency.mjs`, the audit that turns every other drift into a number,
was structurally blind to the one technology where inconsistency is most
expensive. The row said "v5 when stable"; v5 has been in beta for about three
years, npm `latest` is still 4.x, and six repos shipped the beta anyway — two
of them on *different* betas, because a caret on a prerelease spans them.

`better-auth` has a real semver major, so it goes in `blessed-versions.json`
and the existing audit measures it for free. No new machinery. It also uses the
blessed ORM (Drizzle + `pg`), its magic-link plugin takes our own sender so
`mail-kit` slots in unchanged, and hirnli has run it in production since 2026-09.

### What NOT to do

Do **not** extract an auth package. `SHARED.md` lists auth under "what must NOT
be centralized" — coupled to the framework *and* the user schema — and
`sitekit` is the evidence: it centralised nav markup, serves 2 of 20 repos, and
shipped defects consumers could not patch because they did not own the markup.
Centralising the markup centralised the bug.

Share the **decisions** (this section) and the **checks**, never the
implementation. Existing apps migrate **on contact** — when someone is already
in the auth path fixing something — never as a scheduled project. That is the
only way adoption has ever moved here; ADR-0002 sat at "Proposed" for seven
months while the duplicate count went 2 → 4.

## Migration state (2026-09-02: DONE)

Every migration this file opened with has landed, deployed, and been
Expand Down
5 changes: 4 additions & 1 deletion scripts/ci/blessed-versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"then reports every repo below it as a gap. Majors only: 0.x packages",
"(drizzle-orm, @anthropic-ai/sdk, @bitbaum/ai-kit) have no meaningful",
"major and are not listed. next-auth is deliberately absent while v5 is",
"beta. internal_tags: the git tag every consumer of an internal package",
"beta — which is exactly why it stopped being the blessed auth library on",
"2026-09-11: a choice this audit cannot measure is a choice that drifts.",
"better-auth has a real major and is listed below. internal_tags: the git tag every consumer of an internal package",
"must pin until the package is on npm — a package graduates by deleting",
"its entry here (ai-kit did 2026-09: on npm as @bitbaum/ai-kit, 0.x, so",
"untracked like the other 0.x packages)."
Expand All @@ -30,6 +32,7 @@
"lucide-react": 1,
"openai": 7,
"@supabase/supabase-js": 2,
"better-auth": 1,
"pg": 8
},
"internal_tags": {}
Expand Down