The starting point for an app with a React frontend. Same server, env, database and API skeleton as template-htmx — the two differ only in what is served at / — with a React 19 SPA, Tailwind v4 and shadcn/ui on top. Carried from a working project rather than designed in the abstract, and carrying the lessons that cost it time: - The dev script states NODE_ENV=development rather than inheriting it. React resolves its runtime against NODE_ENV: Bun emits jsxDEV() in development and React's production jsx-dev-runtime defines jsxDEV as void 0. Inherit production while transforming for development and the app dies at first paint. Verified: the dev bundle resolves the development runtime and contains no void-0 stub. - Asset URLs in components are relative and <base href="/"> is in the HTML, so a path-prefix proxy can rewrite one attribute and every asset follows. A test enforces it. - Routing is in the hash, because the app may be mounted under a prefix it only learns at runtime and a hash means the same thing at either mount. - index.html holds __PUBLIC_URL__ and gen-index.ts substitutes it, because Bun's HTML bundler fails the build on a root-relative favicon href. - APP_ENV failing closed, env files before the ambient environment, the database enforced by name, one Postgres client per process — as in the htmx template. The database has no tables. The App is a shell with one tab and no features, named as a placeholder. Renaming `app` is the first step and the README lists every place it appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
template-react
A starting point for an app. Bun · Hono · React 19 SPA · Tailwind v4 · shadcn/ui · Postgres with
Drizzle. A JSON API at /api and a React app at /, over one query layer.
1. Make it yours
Everything project-specific is called app. Rename it once, before the first commit:
| Find | Replace with | Where |
|---|---|---|
APP_ENV |
MYPROJECT_ENV |
src/env-file.ts, package.json scripts, test-setup.ts, src/databases/app_db/src/env.ts |
appEnv |
myprojectEnv |
src/env-file.ts and its importers |
appdb / app_db |
myprojectdb / myproject_db |
src/databases/, tsconfig.json paths, package.json scripts |
app / app_dev (database names) |
myproject / myproject_dev |
src/databases/app_db/src/env.ts, .env.example |
"name": "app" |
your package name | package.json |
App |
your brand | src/apps/frontend/App.tsx, index.html <title> |
3000 |
a port of your own | .env.example, src/server.ts |
A case-sensitive search for app, App and APP finds all of it. Thirty seconds now, a chore later.
2. Setup
bun install
cp .env.example .env # BUN_PORT, PUBLIC_URL, POSTGRES_URL_DEV
createdb app && createdb app_dev # or however you make databases
bun run db:push # applies the schema — no tables yet, by design
bun dev
Scripts
bun dev # development on app_dev, with HMR
bun start # production on app
bun test
bun run build # bundle the SPA to dist/
bun run gen:index # index.gen.html from index.html — also run by predev/prestart/prebuild
bun run db:push # schema -> app_dev; APP_ENV=production bun run db:push for production
What is served
| Path | |
|---|---|
/ and every unmatched path |
the SPA — src/apps/frontend |
/api/* |
the JSON API — src/servers/api |
everything in public/ |
from the root |
Configuration, and the rules behind it
APP_ENV, never NODE_ENV. It must be production or development and throws when unset.
Some hosts export NODE_ENV=production into every process they spawn, so anything keyed on it
silently treats development as production.
NODE_ENV is still set by both scripts, and must stay explicit. React resolves its runtime
against it: Bun's bundler emits jsxDEV() calls in development, and React's production
jsx-dev-runtime defines jsxDEV as void 0. Inherit production while transforming for development
and the app dies with "jsxDEV is not a function".
Env files are read with the ambient environment last: .env.production.local (production only)
→ .env.local → .env → process.env.
The database follows from APP_ENV and is enforced before a connection opens; tests cannot reach
production data.
PUBLIC_URL is substituted into index.html by scripts/gen-index.ts because Bun's HTML bundler
fails the build on a root-relative favicon href. It must be the public origin in production.
The placeholder
src/apps/frontend/App.tsx is a shell with one tab and no features. Add the first screen as a feature
folder — CONVENTIONS.md describes the shape. The database has no tables; docs/DATABASE.md
describes adding the first.
Documentation
CLAUDE.md |
The rules an agent needs before touching anything |
CONVENTIONS.md |
React patterns — feature folders, manager hooks — and the project layout |
docs/DATABASE.md |
Drizzle structure, push not migrations, check() vs open text |
docs/SURFACES.md |
The API and the app over one query layer |