ADR 0020: the HTTP runtime, first-run setup and web deployment
Questi contenuti non sono ancora disponibili nella tua lingua.
- Status: accepted; onboarding and integration mode superseded by ADR 0021
- Date: 2026-10-26
Decision
Section titled “Decision”server is the composition root. Only server/config/environment.ts reads the
environment. Configuration is validated before connections open, and secret
values have an explicit reveal() method. HTTP and migration entry points do
nothing when imported. Next.js calls the HTTP runtime lazily on the first request.
The app connects as its dedicated runtime role, with a password derived from
AUTH_SECRET and the instance identifier. The migrator uses the owner’s direct
or session-pooler connection, creates the runtime roles, applies numbered SQL under the migration
lock and reapplies inventory grants. Runtime schema checks use a generated list
of migration checksums, so the web bundle does not read SQL files from disk.
Regenerate that list with pnpm --filter @guestcraft/db migrations:revisions.
First-run setup requires an 80-bit code derived from the instance secret. The migration command prints it privately until setup finishes. The first user, organization, production workspace, audit entries and instance marker are written atomically. Once present, that marker prevents setup from running again, including concurrent attempts. The server never sends the code to a browser.
The deployed HTTP runtime keeps public sign-up closed. Knowing an invited email is not sufficient proof of possession. Token-based invitation onboarding is implemented (ADR 0030); email delivery remains future work. Organization creation is disabled by default; self-service requires explicit configuration. The first operator is created by the protected setup transaction, bypassing public sign-up.
The dashboard renders the active organization and permitted workspaces. Setup,
login and errors use the English and Italian catalogs, negotiated from the browser
or a supported language cookie. All document attributes are rendered on html
before paint. UI modules carry their own client boundaries.
Docker Compose runs PostgreSQL 17, a one-shot migration service and Next.js with
standalone output as a non-root user. Images are pinned by digest. Copy
.env.example and generate separate secrets before docker compose up --build.
The web binds to loopback by default. A remote deployment needs HTTPS and a trusted
ingress; it must not expose owner credentials or trust client-supplied IP headers.
Vercel uploads must retain every workspace manifest referenced by the frozen lockfile. .vercelignore omits analysis artifacts but includes docs/analysis/package.json; a deploy dry run verifies packaging without exposing local environment files.
Vercel is an optional deployment target. The project root is apps/web, Node 24,
and the build is pnpm --filter @guestcraft/web build. Neon Marketplace supplies
pooled and direct URLs. Migration is an explicit deployment step, never a request
side effect. Preview URLs run the same application as other hosted installations.
Customers configure Channex per workspace during onboarding, as ADR 0021 describes.
Verification and remaining work
Section titled “Verification and remaining work”The HTTP integration suite runs migration, setup, sign-in, health, OpenAPI, authorization and schema mismatch checks on PostgreSQL through PgBouncer. Web form tests cover Enter submission, failures, pending input and sign-out. Next.js builds without database secrets. A browser smoke test checks the rendered pages.
The initial SDK and Channex connection onboarding are described in ADR 0021. Operational booking screens, synchronization handlers and the server’s resident worker/cron entry points remain separate work. The job package is integrated with the same database migrations and unit of work; no job handlers are registered by the empty dashboard.
Persistence review correction (2026-10-28)
Section titled “Persistence review correction (2026-10-28)”Database role derivation now accepts an independent GUESTCRAFT_DATABASE_ROLE_SECRET and
GUESTCRAFT_DATABASE_ROLE_ID. Legacy defaults remain available; existing passwords are
preserved and candidate logins verified. Credential rotation stages a new role pair before
promotion and retires the old pair only after drainage. ADR 0038 now permits older compatible runtimes to cold-start across expand-only migrations; contract revisions still require roll-forward. Runtimes built before that compatibility change retain strict journal equality. One session
lock covers journal validation, SQL, role bootstrap, grants and job registration. SQL must
succeed before bootstrap. See installation.
Cookie-authenticated BFF origin boundary (28 October 2026)
Section titled “Cookie-authenticated BFF origin boundary (28 October 2026)”The HTTP composition always injects cookieOriginPolicy into the API’s shared
cookieRequestAllowed boundary. Before authentication, body parsing or route handlers,
all POST, PUT, PATCH and DELETE requests (including bodyless requests, team and identity
routes) must declare an exact trusted Origin: the configured base URL’s origin or an
explicit GUESTCRAFT_TRUSTED_ORIGINS entry. An untrusted or opaque (null) Origin is refused
even when Fetch Metadata claims same-origin. Without Origin, only Sec-Fetch-Site: same-origin or none is accepted; missing, same-site, cross-site and unknown values
are refused. GET, HEAD and OPTIONS remain subject to their ordinary route rules.
Denial uses the localized RFC 9457 request.untrusted_origin problem (403). The
composition logs only the refused origin, shortened to 128 characters with any
path, query, credentials and control characters removed. Sign-in explains this code.
Never
infer trust from Host, Forwarded, Referer, cookies, JSON content types or CORS.
Non-browser cookie clients must send a configured Origin on writes.
The independently authenticated /api/channex/signal/:org/:ws webhook and
GET /api/internal/channex/drain cron are mounted outside this API boundary:
respectively their per-workspace secret and cron bearer secret authorize them, never
the session cookie. First-run setup is a Next.js server action with Next.js origin
checks plus the private setup code and single-use instance marker. Other server
actions retain Next.js’s origin checks. These are narrow composition boundaries,
not path-based exemptions within the cookie API. Better Auth retains its additional
identity-specific CSRF protections. The team gateway also applies the same origin
policy when embedded directly outside the HTTP composition.
Regression evidence includes a production-build Playwright page on a second, same-site localhost origin issuing a credentialed no-cors bodyless drain request. It asserts a 403, actual session-cookie delivery and no new database audit entry, with trusted browser, header-fallback and persisted JSON-write controls. Existing real-database integration tests verify setup, webhook secrets and cron bearer auth.
Response hardening (2026-10-28)
Section titled “Response hardening (2026-10-28)”The Next.js 16 request proxy sets a fresh nonce CSP on each response and forwards the
same policy to dynamic page rendering, which lets Next.js nonce its own scripts.
Every route class receives frame denial and common browser security headers; the API
reference retains its narrower nonce CSP. Authenticated and internal responses are
not cached, while immutable static assets retain their cache behavior. An instance whose
configured GUESTCRAFT_BASE_URL is HTTPS (or that runs on Vercel) sends HSTS without
includeSubDomains, because an operator may use other subdomains; the decision never reads a
request’s X-Forwarded-Proto, which any client can set. The API reference keeps its own
no-referrer policy.
Local loopback HTTP permits development tooling’s eval; hosted origins do not.