Files
pastilhasandClaude Opus 5 c08dab4243 Initial commit: Bun · Hono · React SPA · Postgres/Drizzle template
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>
2026-08-18 02:02:52 +00:00

3.5 KiB

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