No description
  • TypeScript 95.6%
  • Nix 2.3%
  • CSS 1.4%
  • Dockerfile 0.4%
  • HTML 0.3%
Find a file Use this template
hayshin 7e6ff6ff7e
All checks were successful
CI / Code checks (push) Successful in 1m33s
feat(nix): package production application
- add reproducible pnpm flake packages for the API, worker, and migrations
- make the default app supervise the API and worker with clean shutdown
- document Nix build, migration, runtime, and branding workflows
2026-09-12 16:34:13 +05:00
.agents test(web): add Nix-backed Playwright setup 2026-09-12 14:22:41 +05:00
.forgejo/workflows ci: limit workflow to code checks 2026-09-12 14:37:45 +05:00
apps feat(config): adopt secretspec 2026-09-12 16:07:16 +05:00
packages feat: add production-ready full-stack template 2026-09-11 15:33:18 +05:00
.dockerignore feat(config): adopt secretspec 2026-09-12 16:07:16 +05:00
.gitignore feat(config): adopt secretspec 2026-09-12 16:07:16 +05:00
AGENTS.md test(web): add Nix-backed Playwright setup 2026-09-12 14:22:41 +05:00
devenv.lock feat: add production-ready full-stack template 2026-09-11 15:33:18 +05:00
devenv.nix feat(config): adopt secretspec 2026-09-12 16:07:16 +05:00
devenv.yaml feat(config): adopt secretspec 2026-09-12 16:07:16 +05:00
Dockerfile feat(template): harden API contracts and app branding 2026-09-12 15:32:43 +05:00
flake.lock feat(nix): package production application 2026-09-12 16:34:13 +05:00
flake.nix feat(nix): package production application 2026-09-12 16:34:13 +05:00
LICENSE feat: add production-ready full-stack template 2026-09-11 15:33:18 +05:00
package.json fix(ci): build API types before web checks 2026-09-12 14:42:29 +05:00
pnpm-lock.yaml test(web): add Nix-backed Playwright setup 2026-09-12 14:22:41 +05:00
pnpm-workspace.yaml feat: add production-ready full-stack template 2026-09-11 15:33:18 +05:00
README.md feat(nix): package production application 2026-09-12 16:34:13 +05:00
secretspec.toml feat(config): adopt secretspec 2026-09-12 16:07:16 +05:00
tsconfig.json feat: add production-ready full-stack template 2026-09-11 15:33:18 +05:00

Fullstack Template

A production-oriented TypeScript application template built around React, Hono RPC, OpenAPI, Better Auth, Drizzle, PostgreSQL, and Graphile Worker.

Stack

  • React 19, TanStack Router/Query/Form, Tailwind CSS 4, and the repo-owned Visola design system (apps/web/src/components/ui/, tokens in apps/web/src/styles/)
  • Hono on Node.js with Hono RPC and an OpenAPI 3.1 document
  • Better Auth database sessions with email verification and password reset
  • PostgreSQL, Drizzle ORM, and reviewed SQL migrations
  • Graphile Worker and SMTP email delivery (Mailpit locally)
  • Pino structured logs and optional OpenTelemetry OTLP export
  • Vitest, Testing Library, axe, strict TypeScript, and Vite+
  • pnpm workspaces, Nix/devenv, Forgejo CI, and portable OCI images

Architecture

apps/web       Browser SPA. Imports contracts and the API type, never server code.
apps/api       Hono composition, auth, HTTP policies, OpenAPI, and Node lifecycle.
apps/worker    Background task execution and SMTP transport.
packages/contracts      Browser-safe API schemas and DTOs.
packages/db             Drizzle schema, PostgreSQL client, and migrations.
packages/domain         Server-only application services and authorization.
packages/jobs           Typed job contracts and enqueue helpers.
packages/observability  Shared Node logging and telemetry setup.

Production is single-origin. The API serves /api/**, /health/**, generated documentation, and the compiled SPA. Unknown browser routes fall back to index.html; unknown API routes remain JSON 404 responses. The worker is deployed independently and uses the same PostgreSQL database.

Quick start

Prerequisites are provided by devenv. Enter the Nix-provided toolchain with devenv shell in every terminal you use for this project, so node/pnpm resolve to the pinned versions.

devenv shell
pnpm install --frozen-lockfile

SecretSpec resolves the development defaults declared in secretspec.toml when the devenv shell starts. No configuration file needs to be copied first.

Start PostgreSQL and Mailpit in one terminal:

devenv up

Then, from a second shell in the same project (run devenv shell there too), apply migrations:

pnpm db:migrate

Migrations require the database service to already be running, so pnpm db:migrate must come after devenv up, not before.

The default local services are:

Service URL
Web http://localhost:5173
API http://localhost:3000
API reference http://localhost:5173/api/docs
Mailpit http://localhost:8025
PostgreSQL postgresql://localhost:5432/fullstack

Vite proxies API and health requests to Hono, preserving same-origin cookie behavior. Register an account, open its verification message in Mailpit, and follow the link before signing in.

Commands

pnpm dev                 Run app processes (services must already be available)
pnpm build               Build all workspace packages and applications
pnpm check               Run static/type checks
pnpm lint                Run package linters
pnpm fmt                 Format workspace sources
pnpm test                Run unit and component tests
pnpm test:coverage       Run tests with coverage
pnpm test:integration    Run PostgreSQL integration tests
pnpm test:e2e            Run Playwright E2E tests in headless Chromium
pnpm test:e2e:ui         Open Playwright's interactive test UI
pnpm playwright:codegen  Generate Playwright locators against the local app
pnpm db:generate         Generate a reviewed Drizzle SQL migration
pnpm db:migrate          Install Graphile schema and apply Drizzle migrations
pnpm db:check            Validate migration/schema consistency
pnpm db:studio           Open Drizzle Studio
pnpm user:set-role -- user@example.test admin

Run devenv up rather than pnpm dev for the normal local workflow; it also starts PostgreSQL and Mailpit. The devenv shell provides Playwright's browser binaries from Nix. After migrations are applied, pnpm test:e2e reuses the running web and API processes; see apps/web/README.md for agent-oriented and interactive commands.

Coding standards and AI agents

Repository-wide conventions live in AGENTS.md. Focused, progressively loaded workflows are available as repo-local Agent Skills:

  • api-change for contracts, domain behavior, Hono routes, auth, errors, and OpenAPI
  • web-change for React, TanStack Router/Query/Form, browser auth, product UI composition, accessibility, and i18n
  • design-system for shared Visola tokens, typography, motion, ui/ primitives/variants, and the dev-only showcase
  • database-change for PostgreSQL, Drizzle schemas, migrations, and persistence adapters
  • background-job for Graphile tasks, transactional enqueueing, email, retries, and worker lifecycle
  • verify-workspace-change for selecting and reporting the appropriate checks

Skills are selected by need and can be composed. For example, a persisted field shown in the UI normally uses database-changeapi-changeweb-changeverify-workspace-change. A new product screen uses web-change, adding design-system only when shared tokens, primitives, variants, motion, or the showcase itself must change. Library references point to current official documentation rather than storing stale upstream copies.

Configuration

secretspec.toml is the canonical configuration contract. It declares every value, its purpose, and local development defaults. Devenv resolves the default profile from the current environment and exposes it to workspace processes; it does not load dotenv files. The committed BETTER_AUTH_SECRET default is only for local development and must be replaced elsewhere.

Use another SecretSpec provider without changing the repository. For example, devenv --secretspec-provider keyring shell persists local overrides in the system keyring, while the default env provider lets production and CI resolve values already injected by the platform. Run secretspec check inside the devenv shell to validate the active profile.

APP_NAME is the canonical application display name. Browser code receives only VITE_* values, so VITE_APP_NAME is its browser-safe projection — keep both values in sync when renaming (see Rename and rebrand). The generated .pre-commit-config.yaml symlink is produced by devenv's git-hooks integration; do not hand-edit it.

Important production settings:

  • APP_ORIGIN must be the public HTTPS origin.
  • BETTER_AUTH_SECRET must be a strong unique secret.
  • DATABASE_URL should use a least-privilege application role and TLS as required by the provider.
  • Configure SMTP credentials and a verified EMAIL_FROM address.
  • Set OTEL_EXPORTER_OTLP_ENDPOINT only when an OTLP collector is available.
  • Set SIGN_UP_ENABLED=false for invite/admin-provisioned deployments.

Browser code receives only VITE_* values. It calls relative /api paths; there is no production CORS configuration.

Rename and rebrand

The template ships with the Visola sample visual language (double-chevron mark, serif-led type, ruled surfaces) and a neutral App display name. To make it yours:

  1. Set APP_NAME and its VITE_APP_NAME browser projection through your SecretSpec provider. The web header, footer, document titles (index.html uses %VITE_APP_NAME%, runtime pages append env.appName), worker startup logs, and transactional-email footers all follow these values — no source edits needed. For container builds, pass --build-arg APP_NAME=... (see Deployment) so the baked-in initial HTML title matches.
  2. To change the visual identity, edit the semantic --theme-* tokens in apps/web/src/styles/theme.css and replace BrandMark (apps/web/src/components/brand-mark.tsx) plus public/favicon.svg. Visola remains documented as the bundled sample language; keep or remove it as you like.

Sample feature removal checklist

  • Design-system showcase (dev-only): delete apps/web/src/routes/design-system.tsx, apps/web/src/pages/DesignSystem.tsx, and apps/web/src/pages/DesignSystem.test.tsx, then drop the designSystem keys from apps/web/src/locales/en.ts and apps/web/src/locales/ru.ts (see apps/web/README.md).
  • Task sample feature: remove the tasks routes/pages/components, their locale keys, and the corresponding API and job surface.
  • Admin users sample: remove the admin route/page and its API surface.
  • After removal, run pnpm check, pnpm test, and git diff --exit-code -- apps/web/src/routeTree.gen.ts to confirm the generated route tree is clean.

Database workflow

Edit schemas in packages/db, then generate SQL with pnpm db:generate. Review the migration before committing it. Apply migrations as a deployment job before replacing API or worker instances. Neither long-running process performs application migrations at startup.

Graphile Worker owns its PostgreSQL schema. The unified migration command installs/upgrades that schema before applying application migrations. Operations that must atomically update application data and enqueue work should invoke Graphile's SQL add_job function through the same Drizzle transaction.

To grant administration explicitly:

pnpm user:set-role -- user@example.test admin

No account becomes an administrator automatically.

API contracts

Application routes live below /api/v1. Zod schemas in @fullstack/contracts drive runtime validation, Hono RPC inference, and OpenAPI declarations. Better Auth remains under /api/auth and uses its own React client.

  • OpenAPI JSON: /api/openapi.json
  • Scalar reference: /api/docs
  • Liveness: /health/live
  • Readiness: /health/ready

Errors use application/problem+json and include a stable code plus the request ID. Database schemas are deliberately not exported as request DTOs.

Email and jobs

Verification and password-reset callbacks enqueue email.send; SMTP delivery never blocks the request. Payloads are validated both when enqueued and when handled. Graphile Worker retries transient delivery failures with backoff. Sensitive URLs and tokens are not logged.

Mailpit captures all development messages. Production can use any SMTP provider supported by Nodemailer, including SES, Postmark, Mailgun, and SendGrid.

Observability

Pino writes structured JSON to stdout. Request IDs and OpenTelemetry trace/span IDs are attached where available. Headers, cookies, passwords, tokens, SMTP credentials, and job payload secrets are redacted.

Set the OTLP endpoint and optional headers to enable traces and metrics. With no endpoint, the exporter is disabled and local development requires no collector.

Deployment

Build the OCI targets:

docker build --target api -t fullstack-api .
docker build --target worker -t fullstack-worker .
docker build --target migrate -t fullstack-migrate .

The web bundle bakes in the application name at build time from the APP_NAME build argument (default App), so a branded image needs no source edits:

docker build --target api --build-arg APP_NAME=Example -t example-api .

Keep the build argument in sync with the runtime APP_NAME/VITE_APP_NAME values supplied by your SecretSpec provider.

Run migrations once using the dedicated migration target (it installs the Graphile Worker schema, then applies the checked-in Drizzle migrations), deploy the API, then deploy the worker:

docker run --rm --env-file .env --network host fullstack-migrate

The container needs DATABASE_URL (and any DATABASE_MIGRATIONS_FOLDER override) from the environment; adjust --network for how your database is reachable. The API image contains the compiled SPA under /app/public. The images are portable to Cloud Run and other container hosts; provider-specific infrastructure is intentionally outside this template.

Nix flake packaging

flake.nix provides native Nix equivalents of the OCI targets, built with the exact pnpm@11.5.2 from packageManager and fully offline/frozen dependency installation (nixpkgs revision pinned by flake.lock). It does not alter the devenv workflow; git add new files first, since flakes only see Git-tracked sources.

The primary whole-app command starts the API (serving the built SPA) and the worker together under a small supervisor, which forwards INT/TERM to both and exits when either child exits:

nix run .

It needs the same runtime environment as the containers — an external PostgreSQL reachable via DATABASE_URL plus APP_ORIGIN, BETTER_AUTH_SECRET, SMTP settings, and the rest of the secretspec.toml contract — and it never migrates automatically. Apply migrations explicitly first, then start the app:

nix run .#migrate  # installs the Graphile schema, then the checked-in Drizzle migrations
nix run .           # API + worker together

Split deployments (or single-process debugging) use the individual outputs; .#api serves /api, /health, and the SPA, defaulting to NODE_ENV=production, PORT=3000, and the bundled STATIC_DIR:

nix build .#fullstack  # supervised API + worker bundle (also the default package)
nix build .#api        # production API serving the built SPA
nix build .#worker     # Graphile Worker email delivery process
nix build .#migrate    # one-shot migration command
nix run .#api          # API alone; nix run .#worker and nix run .#migrate likewise

Like the Docker build, the web bundle bakes in the application name at build time (default App). For a branded build without source edits:

APP_NAME=Example nix build --impure .#api

Keep it in sync with the runtime APP_NAME/VITE_APP_NAME values. nix flake check runs an offline smoke test asserting the expected entrypoints, exact SPA title, migrations, worker crontab, and launcher are present. Packages are declared for x86_64-linux and aarch64-linux.

Security notes

  • Production cookies are secure, HTTP-only, and same-site.
  • Unsafe browser requests are origin/CSRF checked.
  • Better Auth handles authentication abuse controls; distributed general API rate limiting belongs at the ingress.
  • User-owned resources return 404 when accessed by another user.
  • Readiness checks database/schema availability; liveness does not depend on external services.
  • Never expose the API or database stack traces to clients.

Quality

Repository CI is intentionally a minimal code-check baseline: dependency install, pnpm check (including generated route-tree drift detection), and unit/component tests with coverage. It does not run migration drift detection, PostgreSQL integration tests, E2E, production smoke tests, or OCI builds — consumers are responsible for defining production-grade CI for their deployments. Generated TanStack route trees and Drizzle migrations are committed so that stricter pipelines can check them for drift.