SsuperslateDocs

Architecture

Understand the boundaries between the web app, API, contracts, providers, and data.

Architecture

The foundation separates the browser, API, contracts, persistence, and provider adapters so a builder or coding agent can change product behavior without inventing a second architecture.

flowchart LR
  U[Browser] -->|static assets| W[Native cloud, Vercel, or Cloudflare\nReact SPA]
  U -->|HTTPS + Better Auth cookie| A[Railway, Dokploy, AWS, GCP, or Azure\nBun + Hono API]
  W -. typed Hono RPC .-> A
  A -->|bun:sql| P[(Postgres 18)]
  A -->|Better Auth pg adapter| P
  A -->|normalized provider contract| B[Selected billing provider]
  A -->|transactional delivery| E[Resend]
  A -->|presigned upload + metadata| R[R2, Amazon S3, or GCS]
  A -->|optional envelope tunnel| S[Sentry]

The supported cloud defaults are cost-first and promote explicitly to HA. Each environment selects one backend cloud and one static frontend target. The application is a Vite SPA, not a server-rendered Next.js application, and there is deliberately no generic worker or queue in v1.

Workspace boundaries

PathOwns
apps/webReact shell, React Router, TanStack Query, Zustand client state, CSS Modules, Zag.js, PWA
apps/serverHono routes, services, SQL repositories, Better Auth, migrations, provider orchestration
apps/docsSeparate Fumadocs and Next.js documentation application
packages/contractsZod wire schemas and inferred request/response types
packages/billingProvider-neutral billing types and Polar, Stripe, and Dodo adapters
packages/emailTyped React Email sources and deterministic rendered HTML
packages/create-appBuyer copy, package/brand rewrite, secret/env generation, optional Git initialization
packages/agent-evalPRD outcome checkpoints, intervention records, boundary checks, evidence manifests
packages/agent-contextOptional read-only local MCP documentation and planning context
packages/tsconfigShared compiler policy
infraOpenTofu bootstrap, cloud modules, executable roots, locks, and tests
e2ePlaywright browser flows and deployed-boundary smoke
scripts and .githubRelease, deployment, CI, and verification orchestration

landing/ is the source seller's commercial site. It is outside the root workspace and is never copied into a generated buyer product. operations/ is also seller-only and excluded from buyer output.

Server domain shape

Every product domain uses explicit layers:

contracts schema

routes.ts        HTTP mapping, session, validation, response status

service.ts       policy, provider-neutral orchestration, transactions

repository.ts    parameterized SQL and persistence mapping

Postgres

Routes do not contain SQL. Repositories do not know Hono. Services do not accept raw provider payloads after adapter normalization. Wire schemas live once in packages/contracts; database rows are mapped rather than exposed accidentally.

The optional Add a domain end-to-end playbook provides a tested implementation route. You may use another approach, but preserve the ownership, failure, migration, contract, and test rules above rather than copying a nearby file mechanically.

Identity and authorization

Better Auth owns /auth/*, password hashing, verification, password reset, magic links, Google OAuth, cookies, and sessions. sessionAuth resolves the cookie through auth.api.getSession() and places only session.user.id into Hono context.

Product authorization is separate from authentication:

  1. the route requires a verified session;
  2. the repository receives the current userId;
  3. private reads and writes include the ownership predicate in SQL;
  4. a missing or cross-owner private record returns the same 404 shape when existence must remain private.

There is no application access JWT, refresh token, local-storage token, parallel user key, or generic 403 authorization helper.

See Authentication and Add an authenticated route.

Billing and entitlement flow

The browser may request checkout, but it never grants access:

sequenceDiagram
  participant Browser
  participant API
  participant Provider
  participant DB as Postgres

  Browser->>API: create checkout (session cookie + plan)
  API->>Provider: product/price ID + external user ID
  Provider-->>Browser: hosted checkout
  Provider->>API: signed webhook
  API->>API: verify and normalize in adapter
  API->>DB: claim event + mutate subscription in one transaction
  API->>DB: derive paid entitlement
  Browser->>API: fetch billing status
  API-->>Browser: server-derived access

Subscription/customer rows are provider-neutral. The event claim and state mutation are one transaction; duplicate events are ignored and older provider timestamps cannot restore stale access. Trial, active, past-due, canceled-with-time-remaining, and expired states have explicit policy in domains/billing/entitlements.ts.

Account deletion is blocked while the local provider subscription can renew or an already-paid period remains. Deleting application data does not cancel a remote provider subscription.

Email and local development

React Email source templates are rendered into the server's required HTML inventory. The server preloads all templates before serving traffic. Resend handles production delivery; when Resend is unset in development/test, the complete email and action URL are logged.

The fallback makes authentication deterministic without vendor keys. Production environment validation requires Resend so reset, verification, and magic-link URLs cannot leak through the local log path.

Upload flow

The only included upload purpose is an owner-bound profile image:

  1. validate purpose, MIME allowlist, declared size, target ownership, and entitlement;
  2. persist a pending claim and issue a 15-minute provider presigned PUT;
  3. confirm against actual provider metadata and the pinned object version;
  4. atomically consume the claim or mark it failed and best-effort delete the object.

The path prevents URL-only ownership, replayed confirmation, and trusting the browser's claimed size. Storage remains optional locally. See Add an upload type.

Configuration boundary

apps/server/src/config/env.ts is the sole server environment reader. Zod validates deployment strings, converts them into the typed Config, and enforces atomic optional integrations. Domain code consumes config(); it must not read process.env ad hoc.

Only explicitly public web values use VITE_*. Never place provider tokens, auth secrets, webhook secrets, database URLs, or private DSNs in the web build.

Runtime and deployment lifecycle

The server startup sequence is:

  1. validate environment;
  2. connect to Postgres;
  3. apply pending dbmate migrations;
  4. register application events;
  5. locate and preload every required email template;
  6. bind the Bun server.

The compiled binary still requires external migration SQL and rendered templates. The Docker image copies both. Missing required resources fail startup rather than producing a delayed auth or billing failure.

/health is liveness. /ready queries the initialized database and gates Railway or Dokploy traffic. See Deployment.

State ownership

  • TanStack Query owns browser server-state caching and invalidation.
  • Zustand owns deliberate client-only state, not a copy of the API cache.
  • React Router owns route composition and protected navigation.
  • Postgres is the application system of record.
  • dbmate is the sole schema/migration history.
  • Better Auth is the sole session authority.
  • Provider adapters own raw external payloads and SDK types.
  • Zod contracts own public request and response shapes.

Deliberate limits

The v1 contract excludes multiple frontend frameworks, mobile, enterprise multi-tenancy, seat billing, admin, i18n, a generic job queue, a second billing provider, and a broad infrastructure matrix. These are not missing toggles; adding one requires a validated buyer job and a complete support contract.

Vite+ 0.x and TypeScript 7 are pinned early-adopter dependencies. Their operational rules and package-native escape hatches are in Setup.

On this page