Being Human — Internal Docs

Architecture

How the backend is put together and why

The stack

LayerChoiceWhy
HTTPHono on @hono/node-serverTiny, fast, Web-standard Request/Response, first-class OpenAPI
ValidationZod (schemas in @repo/shared)One schema → runtime validation + TS types + OpenAPI docs
Docs@hono/zod-openapi + ScalarDocs generated from the code, can't drift
ORMDrizzle (Postgres dialect)Typed SQL, real migrations, same code on PGlite and Postgres
Authbetter-authSessions, email+password now, OAuth (Steam/Discord) later
Build / testtsup / VitestFast, zero-config

App factory pattern

Everything is constructed explicitly and passed down — no globals, no import-time side effects:

// index.ts (the only place with side effects)
const env = loadEnv();
const { db, close } = await createDb({ databaseUrl: env.DATABASE_URL });
const auth = createAuth(db, env);
const discord = createDiscordNotifier(env);
const steam = createSteamClient(env);
const app = createApp({ db, auth, env, discord, steam });

The AppContext (src/context.ts) is the seam that makes tests trivial: makeTestApp() builds the same app on an in-memory database and calls app.request() directly — no port, no mocks, no network.

Database strategy

createDb picks a driver by environment, everything downstream is identical:

  • DATABASE_URL set (production): pg Pool against real Postgres.
  • Unset (local dev): PGlite — Postgres compiled to WASM, in-process, persisted to apps/api/.data/pglite.
  • Tests: PGlite fully in-memory, fresh per suite.

Migrations are plain SQL generated by drizzle-kit into apps/api/drizzle/ and run automatically on boot on all three. See Database.

Route structure

Each route group is a factory taking AppContext and returning an OpenAPIHono sub-app with createRoute definitions built from @repo/shared schemas:

src/routes/wishlist.ts      → /api/wishlist
src/routes/invites.ts       → /api/invites
src/routes/achievements.ts  → /api/achievements

app.ts mounts them, plus better-auth's handler on /api/auth/*, the Scalar UI on /docs, and the spec on /openapi.json.

Where multiplayer will live

Not in this API. The Rust sim is deterministic (same seed + inputs → identical world, byte for byte), which makes lockstep networking the natural fit — that wants a dedicated realtime server (likely Rust, reusing the sim crate), not an HTTP framework. This API stays the authority for identity: game servers validate better-auth session tokens against it. Meta-services that fit HTTP fine — leaderboards, cloud saves, matchmaking brokering — land here.

On this page