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>
2.3 KiB
app
A project started from template-react. Rename app first — README step 1.
Bun · Hono · React 19 · Tailwind v4 · shadcn/ui · Postgres with Drizzle.
Read these before writing code
CONVENTIONS.md |
React patterns — feature folders, manager hooks, colocated state — and layout |
docs/DATABASE.md |
Drizzle structure, push not migrations, check() vs open text |
docs/SURFACES.md |
The JSON API and the app: one process, one query layer |
README.md |
Setup, scripts, configuration |
Rules that are load-bearing
APP_ENV, never NODE_ENV. Must be production or development; throws when unset. Hosts export
NODE_ENV=production into everything they spawn, so anything keyed on it treats development as
production silently. NODE_ENV is still set explicitly by both scripts because React's runtime
resolves against it — Bun emits jsxDEV() in development and React's production runtime defines it
as void 0.
Env files are read with the ambient environment LAST: .env.production.local (production only)
→ .env.local → .env → process.env.
Asset URLs in components are relative — never start with /. <base href="/"> in index.html
is what a path-prefix proxy rewrites; a leading slash bypasses it. asset-urls.test.ts enforces this.
fetch('/api/...') is the exception.
Routing is in the hash. The app may be mounted under a prefix it only learns at runtime, and a
hash means the same thing at either mount point. lib/routes.ts.
One feature folder per screen, one manager hook per feature. State lives in the hook, components
receive the manager. CONVENTIONS.md.
Serializers live in src/servers/serialize.ts only. Its test walks the module's exports for
camelCase keys and for serializers without a case.
Bun
bun <file>notnode,bun testnot jest/vitest,bun install,bunxBun.serve()— not express.Bun.fileovernode:fs.Bun.$over execa- Bun loads
.envautomatically; do not add dotenv bun devandbun startset the env markers. Runningsrc/server.tsdirectly throws, by design
Before you finish
bunx tsc --noEmitandbun test- Verify claims rather than asserting them — start the server and request the thing
- Commit with a message that says why, not what the diff already shows