- TypeScript 95.6%
- Nix 2.3%
- CSS 1.4%
- Dockerfile 0.4%
- HTML 0.3%
|
All checks were successful
CI / Code checks (push) Successful in 1m33s
- 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 |
||
|---|---|---|
| .agents | ||
| .forgejo/workflows | ||
| apps | ||
| packages | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| devenv.lock | ||
| devenv.nix | ||
| devenv.yaml | ||
| Dockerfile | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| secretspec.toml | ||
| tsconfig.json | ||
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 inapps/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-changefor contracts, domain behavior, Hono routes, auth, errors, and OpenAPIweb-changefor React, TanStack Router/Query/Form, browser auth, product UI composition, accessibility, and i18ndesign-systemfor shared Visola tokens, typography, motion,ui/primitives/variants, and the dev-only showcasedatabase-changefor PostgreSQL, Drizzle schemas, migrations, and persistence adaptersbackground-jobfor Graphile tasks, transactional enqueueing, email, retries, and worker lifecycleverify-workspace-changefor 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-change → api-change → web-change → verify-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_ORIGINmust be the public HTTPS origin.BETTER_AUTH_SECRETmust be a strong unique secret.DATABASE_URLshould use a least-privilege application role and TLS as required by the provider.- Configure SMTP credentials and a verified
EMAIL_FROMaddress. - Set
OTEL_EXPORTER_OTLP_ENDPOINTonly when an OTLP collector is available. - Set
SIGN_UP_ENABLED=falsefor 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:
- Set
APP_NAMEand itsVITE_APP_NAMEbrowser projection through your SecretSpec provider. The web header, footer, document titles (index.htmluses%VITE_APP_NAME%, runtime pages appendenv.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. - To change the visual identity, edit the semantic
--theme-*tokens inapps/web/src/styles/theme.cssand replaceBrandMark(apps/web/src/components/brand-mark.tsx) pluspublic/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, andapps/web/src/pages/DesignSystem.test.tsx, then drop thedesignSystemkeys fromapps/web/src/locales/en.tsandapps/web/src/locales/ru.ts(seeapps/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, andgit diff --exit-code -- apps/web/src/routeTree.gen.tsto 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.