Skip to content

Latest commit

 

History

History
271 lines (205 loc) · 19.9 KB

File metadata and controls

271 lines (205 loc) · 19.9 KB
title Developer Onboarding
description Get the app running, learn the codebase layout, and ship your first change.
group General
order 40

Developer Onboarding

Audience: developers joining the codebase. Get it running, learn the layout, and ship your first change.


1. Prerequisites

Tool Version Notes
Node.js 24.x Pinned via .nvmrc (24) and package.json engines (node 24.x, pnpm >=10); CI, the Docker image and Vercel all run Node 24 — keep them on the same major. engines.node names the exact major on purpose: an open range (>=24) would let Vercel move production to a newer major on its own (a Node-24-only failure inside the Next runtime once passed every check on Node 22 CI, see #400).
pnpm 10.33.2 Pinned via package.json → packageManager, with the release's +sha512.… integrity hash (review #226) — Corepack verifies the tarball before running it. Enable with corepack enable; change the version only with corepack use pnpm@<version>, which rewrites the hash too.
Docker recent Only used to run PostgreSQL locally. A managed Postgres works too.
PostgreSQL 17 The local Docker image is pgvector/pgvector:pg17.
openssl (or any CSPRNG) — For generating secrets.
Git — —

Windows note: the repo is developed on Windows and POSIX shells. Commands below are POSIX; PowerShell equivalents are noted where they differ.

2. Clone & install

git clone <repository-url> devresponsekit
cd devresponsekit
corepack enable          # makes the pinned pnpm available
pnpm install             # installs from the frozen lockfile
cp .env.example .env      # PowerShell: Copy-Item .env.example .env

Adding or updating a dependency? .npmrc sets minimum-release-age=1440, so pnpm will not RESOLVE a version published in the last 24 hours (review #226 — see SECURITY.md, "Install-time supply chain"). Installs from the lockfile are unaffected; to take an urgent security release inside that window, run the one command with --config.minimum-release-age=0 and say why in the PR.

Then set, at minimum, BETTER_AUTH_SECRET in .env to a strong random value (the default is a placeholder). Generate one with:

openssl rand -base64 32

To launch SSO handoffs locally (the satellite rig, or the SSO e2e) the primary also needs SSO_HANDOFF_PRIVATE_KEY — an Ed25519 private JWK:

node -e "import('jose').then(async j=>{const {privateKey}=await j.generateKeyPair('EdDSA',{extractable:true});console.log(JSON.stringify(await j.exportJWK(privateKey)))})"

See Configuration for the full variable list.

3. Run locally

pnpm db:up            # start PostgreSQL in Docker (host port 5444)
pnpm db:provision     # one shot: Better Auth tables + app schema + seed (auth:migrate → app:migrate → seed)
pnpm dev              # start the dev server → http://localhost:3000

All tables are deployed into the auth schema (configurable via DB_SCHEMA; the migrate steps create it automatically). If you inspect the DB with psql, the tables won't be in public — use \dt auth.* or SET search_path = auth, public;. See Configuration → DB_SCHEMA.

db:provision / db:seed are safe to re-run. The seed's inserts are on conflict do nothing, and its only update — relaxing the platform sign-up default to auto_active (verify → active, no approval step) — runs only on a never-administered row. If you changed the policy under Administrator → Platform sign-up defaults, a re-seed keeps your setting and logs [seed] platform sign-up policy left as configured (admin-managed). See Sign-up policy §5.

Sign in with the seeded admin (defaults from .env):

  • Email: admin@devresponse.local
  • Password: ChangeMe-LocalOnly-123!

Want multi-tenant test data? Load the dev fixture — 3 organizations × 7 users (all pre-approved), plus 3 cross-org members (one account in all three orgs), two groups with members, and back-dated registrations + audit history so the dashboard charts and recent-activity feed are populated:

pnpm db:seed:dev

The dev fixture is local-only by construction. Before opening a connection it refuses (exit 1, nothing written) unless the DATABASE_URL host is local — localhost, 127.0.0.1, ::1, 0.0.0.0, or no host — regardless of NODE_ENV, and it separately refuses under NODE_ENV=production. A Neon / RDS / any hosted URL in your .env therefore cannot be seeded by accident. If you genuinely want the fixture on a remote disposable database, pass --force (pnpm db:seed:dev --force) or set DEV_SEED_ALLOW_REMOTE=1; NODE_ENV=production additionally needs DEV_SEED_ALLOW_PROD=1. pnpm db:reset applies the same host check (src/db/guards.ts).

Every fixture account shares the password DevPassword123! (override with DEV_SEED_PASSWORD):

Account Authority
superuser@orga.local (also orgb/orgc) Cross-organization superadmin
orgadmin@orga.local (also orgb/orgc) Full admin.* catalog, scoped to that one org
user1..5@orga.local (also orgb/orgc) Plain member — shell.view only
multi1..3@shared.local Member of all three orgs (exercises the org switcher)

Need a clean slate? (Destructive — local only.)

pnpm db:reset          # DRY RUN: lists what it would drop, changes nothing
pnpm db:reset:reload   # drop everything, re-migrate, and re-seed in one step

4. Quality gates (run before every PR)

pnpm typecheck      # tsc --noEmit
pnpm lint           # eslint .
pnpm format:check   # prettier --check  (use `pnpm format` to auto-fix)
pnpm test:coverage  # vitest with the coverage ratchet

The DB-backed suite runs against a real, migrated Postgres: the database in DATABASE_TEST_URL, or DATABASE_URL when that is unset. It refuses a host that is not local, like db:seed:dev. .env.example's DATABASE_TEST_URL names devresponse_db_test, which you create and migrate once (Testing §3 has the commands):

pnpm test:db        # vitest run --config vitest.db.config.ts

Slower, browser-based suites (also run in CI):

pnpm build          # next build  (catches config/server errors early)
pnpm test:e2e       # Playwright end-to-end
pnpm test:a11y      # Playwright + axe-core accessibility

There's a single convenience target that runs the whole gate:

pnpm test:all

See Testing for details on each suite, the sharded runner, and the coverage ratchet.

5. Project structure

src/
├── app/
│   ├── (root)/                     # locale-independent entry (root layout, error)
│   ├── [locale]/                   # everything localized lives here
│   │   ├── (public)/               # marketing/landing, about, public docs
│   │   ├── (auth)/                 # sign-in, sign-up, verify-email, invite, reset, pending/blocked
│   │   └── (secure)/               # authenticated app — authz boundary is here
│   │       ├── layout.tsx          # loads access context + decideSecureAccess
│   │       └── app/
│   │           ├── account/        # profile, preferences, security, api-keys
│   │           ├── docs/           # in-app docs viewer
│   │           └── administrator/  # the admin console
│   │               └── _components/administrator-navigation.ts  # canonical nav
│   └── api/                        # HTTP API surface
│       ├── auth/[...all]/          # Better Auth catch-all
│       ├── account/ · preferences/ · navigation/
│       ├── sso/                    # launch + consume (handoff)
│       ├── docs/asset/             # docs images
│       └── v1/                     # versioned machine API
├── lib/
│   ├── auth.ts                     # Better Auth config
│   ├── auth-status.ts              # getUserAccessContext, decideSecureAccess
│   ├── admin/                      # guards, access-scope, rate-limit, audit, list-query
│   ├── api-auth/                   # API keys, JWT, scopes, caller resolution
│   ├── account/ · email/ · sso*    # self-service, outbox email, SSO handoff
│   └── env.ts                      # environment loading/validation
├── db/
│   ├── database.ts                 # Kysely instance + pg pool (shared with Better Auth)
│   ├── schema/app-schema.ts        # table types
│   ├── migrations/                 # 0001-initial-schema.sql + runners
│   ├── seeds/                      # seed-local.ts, dev-init.ts
│   └── reset-database.ts           # destructive reset tooling
├── components/                     # shadcn/ui, app shell, data grid, navigation
├── i18n/                           # next-intl request config
└── messages/                       # en.json, fr.json, es.json, uk.json, pt.json, zh.json, hi.json, ja.json

tests/                              # unit, component, integration, security, e2e, accessibility
scripts/test-shards.mjs            # sharded vitest runner
docker/postgres/init/              # Postgres init SQL (extensions)

6. Main entry points

Entry point File
Edge proxy (redirect + locale) src/proxy.ts
Root layout src/app/(root)/layout.tsx
Locale layout src/app/[locale]/layout.tsx
Authorization boundary src/app/[locale]/(secure)/layout.tsx
Admin console nav (source of truth) src/app/[locale]/(secure)/app/administrator/_components/administrator-navigation.ts
Better Auth config src/lib/auth.ts
Access-context resolution src/lib/auth-status.ts
Scope primitives src/lib/admin/access-scope.server.ts
DB connection src/db/database.ts
Next.js config (headers, plugins) next.config.mjs

7. Coding conventions (discovered from the repo)

  • Server-first. Components are Server Components unless they need interactivity; add "use client" only at the boundary. Server-only modules end in .server.ts and/or import server-only.
  • TypeScript strict. pnpm typecheck must pass with zero errors. Note noUncheckedIndexedAccess is on — indexed access yields T | undefined.
  • Validate at the edge with Zod. Route handlers parse request bodies with a Zod schema and return a uniform error envelope on failure.
  • Authorize through the primitives. Never re-derive tenant scope inline — call requireAdminPermission / resolveOrgScope / canAccessOrg. An admin route that doesn't reference a scope primitive fails the CI invariant test.
  • Rate-limit every admin mutation. Each POST/PATCH/DELETE admin handler calls enforceRateLimit(...) right after the permission check (also enforced by an invariant test).
  • Audit every mutation. Use the audit*Action helpers; pass the request so the x-request-id is correlated.
  • Internationalize all user-facing text. Every leaf key must exist in all 8 locale files (en, fr, es, uk, pt, zh, hi, ja) — a parity test enforces it. Add keys to en.json first, then the rest.
  • Format dates and numbers through the app formatter. const format = useAppFormatter() in a client component, const format = await getAppFormatter(locale) in a server one, then format.date / format.dateTime / format.number. It applies the viewer's saved time zone, date format and number format, and renders the same text on the server and in the browser. A new Intl.DateTimeFormat, toLocaleString() or next-intl useFormatter under src/app or src/components fails an invariant test (F-37).
  • Formatting & linting are enforced by Prettier (with the Tailwind plugin) and ESLint (eslint-config-next + typescript-eslint). Run pnpm format before committing.
  • Commit & PR hygiene (from project memory): land each logically-complete change as its own PR; PRs auto-merge on green; don't pipe pnpm build through head/Select -First (it truncates and breaks the build log) — redirect to a file instead.

8. How to add a feature

A typical admin feature (mirror an existing one such as Roles or Groups):

  1. Schema (if needed): add a new numbered forward migration (src/db/migrations/000N-….sql) — 0001-initial-schema.sql is frozen/append-only, never edit it — and add types to src/db/schema/app-schema.ts.
  2. Permissions: add keys to ADMIN_PERMISSION_CATALOG in src/lib/admin/permissions.ts (they flow into the admin.platform/superuser roles automatically). Update the catalog-count test.
  3. API: add a route handler under src/app/api/administrator/<feature>/.... Use requireAdminPermission, resolveOrgScope/canAccessOrg, enforceRateLimit, Zod validation, the list-query helper, the error envelope, and an audit call.
  4. UI: add pages under src/app/[locale]/(secure)/app/administrator/<feature>/ and a nav entry in administrator-navigation.ts with a requires permission. Reuse the shared DataGrid and form patterns.
  5. i18n: add strings to all 8 src/messages/*.json files (en, fr, es, uk, pt, zh, hi, ja).
  6. Tests: integration tests for the routes, component tests for client UI, and update the invariant/coverage tests as needed.
  7. Docs: update the relevant file in /docs.

Browser-observable change? Verify it in a running instance rather than asking a reviewer to check manually.

9. Debugging locally

9.1 Where to look first

  • Server logs print to the pnpm dev terminal — server component and route-handler errors land here.
  • Liveness/readiness: GET /api/health (process up) and GET /api/health/ready (env valid, database reachable, both schemas migrated). A 503 names its reason: database_unreachable means check Postgres before anything else, schema_behind means run pnpm db:auth:migrate && pnpm db:app:migrate and restart pnpm dev, and config_invalid means a variable in .env (the terminal names it).
  • Request correlation: every admin and v1 response carries x-request-id, a thrown 500 internal_error included (bar the public, cacheable /api/v1/jwks.json and /api/v1/openapi.json); grep the terminal log, app_audit_events (and Sentry, if enabled) for that id. A new route handler must be exported through withAdminRoute / withV1Route (src/lib/route-handler.server.ts), or tests/unit/route-request-id-invariant.test.ts fails. The audit trail is often the fastest answer to "what did the app actually do?"

9.2 Database inspection

  • Connect with any Postgres client to postgresql://devresponse:devresponse@localhost:5444/devresponse_db.
  • Every table lives in the auth schema (DB_SCHEMA), not public — use \dt auth.* or SET search_path = auth, public;. If you find app tables in public, a migration/seed ran without the connection-level search path: DB_SEARCH_PATH_VIA_OPTIONS=0 is set. That flag exists only for transaction-pooling endpoints (Neon pooled, PgBouncer) paired with a server-side ALTER ROLE … SET search_path; against local/direct Postgres, leave it unset. Recover by unsetting it, dropping the stray public tables, and re-running pnpm db:reset:reload.
  • Inspect app_audit_events to see what the app recorded for an action; app_sso_handoff_nonces shows one row per SSO launch (consumed_at stamps on use — a null for an old token means the consume POST never arrived).

9.3 Auth & session issues

  • Check the session / account tables and the Better Auth catch-all responses; confirm BETTER_AUTH_URL matches the origin you're hitting.
  • Remember cookies are host-scoped, port-agnostic: two apps on localhost (any ports) share the same better-auth.session_token cookie slot and will clobber each other; put local test apps on distinct hostnames (see §9.5) when that matters.

9.4 Email in dev

With no EMAIL_PROVIDER, messages are written to app_outbox with status logged and visible under Administrator → Email — nothing is ever sent.

The bodies shown there are redacted: a password-reset, verification or invitation link reads …/reset-password/[redacted]?… / …?token=[redacted] because the admin outbox must never hand an org admin a live credential (review #21). To follow such a link locally, read the DB-only delivery_payload column (never served by the API):

select delivery_payload->>'text' from app_outbox
 where to_email = 'you@example.com' order by created_at desc limit 1;

The e2e suites do the same through tests/e2e/helpers/outbox-db.ts.

9.5 The local SSO / satellite rig

To debug cross-subdomain SSO (or any multi-app flow) on one machine, use the suggested subdomain setup in the Satellite Apps Integration Guide §6.6: the kit on http://devresponse.local:3000 and the three satellites on app1/app2/app3.devresponse.local — true subdomains that mirror a live fleet (per-subdomain session cookies for the handoff apps, which still receive the primary's parent-domain cookie, and a parent-domain shared session for Option C). It is one security domain on one database: none of its satellites is contained (§1.1). One elevated run of scripts/setup-local-subdomains.ps1 maps the four hosts to 127.0.0.1 (idempotent; -Remove undoes it); *.localtest.me is the no-admin-rights fallback. The guide includes the copy-paste steps (seed, secret, SQL registration, per-app env, next dev -H …) and the two dev-only gotchas (host binding for absolute URLs; the CSP upgrade-insecure-requests directive silently killing form POSTs on http non-localhost hosts).

10. Common mistakes & troubleshooting

Symptom Likely cause / fix
pnpm install fails on version Run corepack enable so the pinned pnpm 10.33.2 is used.
App can't reach the database Is pnpm db:up running? Is the port 5444 (not 5432)? Check DATABASE_URL.
Boot error about a secret/JWK BETTER_AUTH_SECRET unset, API_JWT_ENABLED=1 without API_JWT_PRIVATE_KEY, or an Ed25519 key (SSO_HANDOFF_* / API_JWT_*, previous keys included) that cannot be used: the schema checks each one's shape and the Node boot hook imports it, so a truncated value, a stray quote or a mismatched x fails here. The error names the variable and the rule.
Boot error about a URL An origin-valued variable (BETTER_AUTH_URL, SSO_HANDOFF_ISSUER, ADMIN_TRUSTED_ORIGINS, …) is not an http(s) origin, or COOKIE_DOMAIN does not cover BETTER_AUTH_URL. http://localhost:3000 is always fine; see Configuration §1.
/api/sso/launch returns 503 sso_not_configured No SSO_HANDOFF_PRIVATE_KEY on this instance — it can consume handoffs but not issue them.
403/404 on an admin call you expected to work Tenant scope — a non-superadmin only sees their own org; out-of-scope resources return 404 by design.
Tables ended up in public instead of auth DB_SEARCH_PATH_VIA_OPTIONS=0 is set locally — a pooler-only setting. Unset it, drop the strays, re-run pnpm db:reset:reload (see §9.2).
Locale-parity test fails A new text key is missing from one of the 8 locale files (en, fr, es, uk, pt, zh, hi, ja). Add it everywhere.
Coverage gate fails but tests pass New untested code dropped global coverage below the ratchet — add tests (the local sharded runner does not compute coverage; run pnpm test:coverage).
Flaky/odd test failures with "not a function" Run the sharded runner (pnpm test), not a single in-process Vitest run — see Testing.

More in Troubleshooting.


Next: API Reference · Testing