From 45cb11788c6991c87cdcf3ade67350d56a558d0f Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 24 Jul 2026 11:08:00 +0800 Subject: [PATCH 01/10] feat: add /health-check endpoint, default API port 8000, show dev URLs - backends (hono/elysia) expose read-only GET /health-check; with an ORM selected it runs a DB query and returns rows to confirm wiring - default backend PORT 8000 (was 3001) - success screen shows web/api localhost URLs - add clean.sh dev helper to remove scaffolded folder Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- clean.sh | 4 +++ .../backend/elysia/server/index.ts.tmpl | 27 +++++++++++++++++- .../backend/hono/server/index.ts.tmpl | 28 ++++++++++++++++++- internal/tui/success.go | 4 +++ 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100755 clean.sh diff --git a/README.md b/README.md index 03011d5..ca83c49 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ my-app/ domain/ # shared contract: zod schemas (with --validation zod) or plain types ``` -- **`--backend hono`** runs on Node via `tsx`; **`--backend elysia`** runs on Bun. `pnpm dev` runs `apps/web` and `apps/api` together. +- **`--backend hono`** runs on Node via `tsx`; **`--backend elysia`** runs on Bun. `pnpm dev` runs `apps/web` (`http://localhost:3000`) and `apps/api` (`http://localhost:8000`) together. Every backend exposes `GET /health-check`; with an ORM selected it also runs a read-only query against the database and returns the rows, so you can confirm the DB wiring end-to-end. - **`--orm drizzle` / `--orm prisma`** add the config, a `db/` client, and `.env.example` under `apps/api`. `web` and `api` both depend on `packages/domain` via `workspace:*`. - **`--db postgres` / `--db mysql`** also generate a root `docker-compose.yml` whose credentials match `.env.example`, so `docker compose up -d` gives you a working database. `sqlite` needs nothing extra; `d1` targets Cloudflare Workers (pair with `--deploy cloudflare-workers`). diff --git a/clean.sh b/clean.sh new file mode 100755 index 0000000..4b5e831 --- /dev/null +++ b/clean.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +# Remove the scaffolded my-app folder (dev cleanup). Pass a name to remove a +# different folder: ./clean.sh some-app +rm -rf "${1:-my-app}" diff --git a/config/templates/backend/elysia/server/index.ts.tmpl b/config/templates/backend/elysia/server/index.ts.tmpl index baa0c2d..b23627d 100644 --- a/config/templates/backend/elysia/server/index.ts.tmpl +++ b/config/templates/backend/elysia/server/index.ts.tmpl @@ -1,7 +1,32 @@ import { Elysia } from "elysia"; +{{- $orm := printf "%s" .ORM }} +{{- $hasDb := and (ne $orm "none") (ne (printf "%s" .Database) "d1") }} +{{- if $hasDb }} +import { db } from "../db"; +{{- if eq $orm "drizzle" }} +import { users } from "../db/schema"; +{{- end }} +{{- end }} const app = new Elysia() .get("/", () => ({ message: "Hello from {{ .ProjectName }}" })) - .listen(Number(process.env.PORT ?? 3001)); + // Health check — read-only; verifies the DB connection with a lightweight query. + .get("/health-check", async () => { +{{- if $hasDb }} + try { +{{- if eq $orm "drizzle" }} + const rows = await db.select().from(users); +{{- else }} + const rows = await db.user.findMany(); +{{- end }} + return { status: "ok", db: "connected", users: rows }; + } catch (err) { + return { status: "error", db: "disconnected", error: String(err) }; + } +{{- else }} + return { status: "ok" }; +{{- end }} + }) + .listen(Number(process.env.PORT ?? 8000)); console.log(`Server running on http://localhost:${app.server?.port}`); diff --git a/config/templates/backend/hono/server/index.ts.tmpl b/config/templates/backend/hono/server/index.ts.tmpl index c1033a5..718dfa6 100644 --- a/config/templates/backend/hono/server/index.ts.tmpl +++ b/config/templates/backend/hono/server/index.ts.tmpl @@ -1,11 +1,37 @@ import { serve } from "@hono/node-server"; import { Hono } from "hono"; +{{- $orm := printf "%s" .ORM }} +{{- $hasDb := and (ne $orm "none") (ne (printf "%s" .Database) "d1") }} +{{- if $hasDb }} +import { db } from "../db"; +{{- if eq $orm "drizzle" }} +import { users } from "../db/schema"; +{{- end }} +{{- end }} const app = new Hono(); app.get("/", (c) => c.json({ message: "Hello from {{ .ProjectName }}" })); -const port = Number(process.env.PORT ?? 3001); +// Health check — read-only; verifies the DB connection with a lightweight query. +app.get("/health-check", async (c) => { +{{- if $hasDb }} + try { +{{- if eq $orm "drizzle" }} + const rows = await db.select().from(users); +{{- else }} + const rows = await db.user.findMany(); +{{- end }} + return c.json({ status: "ok", db: "connected", users: rows }); + } catch (err) { + return c.json({ status: "error", db: "disconnected", error: String(err) }, 503); + } +{{- else }} + return c.json({ status: "ok" }); +{{- end }} +}); + +const port = Number(process.env.PORT ?? 8000); serve({ fetch: app.fetch, port }, (info) => { console.log(`Server running on http://localhost:${info.port}`); }); diff --git a/internal/tui/success.go b/internal/tui/success.go index e6cca3a..80797c6 100644 --- a/internal/tui/success.go +++ b/internal/tui/success.go @@ -55,6 +55,10 @@ func PrintSuccess(cfg pkg.ProjectConfig) { if cfg.Layout.IsMonorepo() && cfg.Backend != "none" { runLine += MutedStyle.Render(" # runs apps/web + apps/api") } + runLine += MutedStyle.Render(" → web http://localhost:3000") + if cfg.Backend != "none" { + runLine += MutedStyle.Render(", api http://localhost:8000") + } cmds := fmt.Sprintf( "\n\n %s%s\n %s\n %s", From 251f7109d62477bad66cbfaa5458d1eec00649d5 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 24 Jul 2026 11:25:36 +0800 Subject: [PATCH 02/10] fix: load .env in tsx dev server, move dev URLs to own section, add clean.sh feedback - hono dev:server uses tsx --env-file-if-exists=.env so DATABASE_URL loads on Node (Bun/drizzle-kit/prisma already auto-load .env) - success screen shows a dedicated 'Local URLs:' section instead of an inline comment on the dev command - clean.sh reports what it removed (or that nothing existed) Co-Authored-By: Claude Opus 4.8 (1M context) --- clean.sh | 8 +++++++- config/registry.json | 2 +- internal/tui/success.go | 12 ++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/clean.sh b/clean.sh index 4b5e831..bcc875a 100755 --- a/clean.sh +++ b/clean.sh @@ -1,4 +1,10 @@ #!/usr/bin/env sh # Remove the scaffolded my-app folder (dev cleanup). Pass a name to remove a # different folder: ./clean.sh some-app -rm -rf "${1:-my-app}" +target="${1:-my-app}" +if [ -e "$target" ]; then + rm -rf "$target" + echo "Removed $target" +else + echo "Nothing to remove: $target does not exist" +fi diff --git a/config/registry.json b/config/registry.json index 23a2de5..42e733e 100644 --- a/config/registry.json +++ b/config/registry.json @@ -564,7 +564,7 @@ "tsx": "^4.19.2" }, "scripts": { - "dev:server": "tsx watch server/index.ts" + "dev:server": "tsx watch --env-file-if-exists=.env server/index.ts" } } }, diff --git a/internal/tui/success.go b/internal/tui/success.go index 80797c6..92dfd3b 100644 --- a/internal/tui/success.go +++ b/internal/tui/success.go @@ -55,10 +55,6 @@ func PrintSuccess(cfg pkg.ProjectConfig) { if cfg.Layout.IsMonorepo() && cfg.Backend != "none" { runLine += MutedStyle.Render(" # runs apps/web + apps/api") } - runLine += MutedStyle.Render(" → web http://localhost:3000") - if cfg.Backend != "none" { - runLine += MutedStyle.Render(", api http://localhost:8000") - } cmds := fmt.Sprintf( "\n\n %s%s\n %s\n %s", @@ -68,6 +64,14 @@ func PrintSuccess(cfg pkg.ProjectConfig) { runLine, ) + // Local URLs the dev server serves on. + urls := "\n\n " + AccentStyle.Render("Local URLs:") + + "\n " + MutedStyle.Render("web ") + orange.Render("http://localhost:3000") + if cfg.Backend != "none" { + urls += "\n " + MutedStyle.Render("api ") + orange.Render("http://localhost:8000") + } + cmds += urls + // Monorepo layout: explain the pnpm-workspace structure. if cfg.Layout.IsMonorepo() { ws := "\n\n " + AccentStyle.Render("Workspace (pnpm):") + From b2d3358bd819f79a56884d4dc527b00e7a8dbd88 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 24 Jul 2026 11:40:44 +0800 Subject: [PATCH 03/10] feat: agent-ready scaffolding + fix sqlite drizzle db path (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGENTS.md is now stack-aware: real commands, dev URLs, monorepo map, ORM/DB workflow (incl. db:seed), and /health-check for the chosen combo - add .claude/settings.json permission allowlist (package manager, docker compose for server DBs, wrangler for Cloudflare, read-only git) - add .claude/commands: /verify, /format-fix, /new-component - add db:seed script + seed fixtures for drizzle and prisma (guards D1); bundles tsx as the seed runner - fix: drizzle sqlite client stripped the file: URL scheme so better-sqlite3 opens the right path — the same DATABASE_URL now works for drizzle-kit and the runtime (previously /health-check and db:seed failed on sqlite) Verified end-to-end on a live sqlite project: install → db:generate → db:migrate → db:seed → GET /health-check returns the seeded rows. Closes #108 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 10 ++- config/registry.json | 12 ++- config/templates/orm/drizzle/db/index.ts.tmpl | 4 +- config/templates/orm/drizzle/db/seed.ts.tmpl | 20 +++++ .../templates/orm/prisma/prisma/seed.ts.tmpl | 16 ++++ .../.claude/commands/format-fix.md.tmpl | 6 ++ .../.claude/commands/new-component.md.tmpl | 15 ++++ .../shared/.claude/commands/verify.md.tmpl | 17 ++++ .../shared/.claude/settings.json.tmpl | 27 +++++++ config/templates/shared/AGENTS.md.tmpl | 79 ++++++++++++++++--- 10 files changed, 187 insertions(+), 19 deletions(-) create mode 100644 config/templates/orm/drizzle/db/seed.ts.tmpl create mode 100644 config/templates/orm/prisma/prisma/seed.ts.tmpl create mode 100644 config/templates/shared/.claude/commands/format-fix.md.tmpl create mode 100644 config/templates/shared/.claude/commands/new-component.md.tmpl create mode 100644 config/templates/shared/.claude/commands/verify.md.tmpl create mode 100644 config/templates/shared/.claude/settings.json.tmpl diff --git a/README.md b/README.md index ca83c49..0a22b53 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,19 @@ my-app/ ``` - **`--backend hono`** runs on Node via `tsx`; **`--backend elysia`** runs on Bun. `pnpm dev` runs `apps/web` (`http://localhost:3000`) and `apps/api` (`http://localhost:8000`) together. Every backend exposes `GET /health-check`; with an ORM selected it also runs a read-only query against the database and returns the rows, so you can confirm the DB wiring end-to-end. -- **`--orm drizzle` / `--orm prisma`** add the config, a `db/` client, and `.env.example` under `apps/api`. `web` and `api` both depend on `packages/domain` via `workspace:*`. +- **`--orm drizzle` / `--orm prisma`** add the config, a `db/` client, `.env.example`, and `db:generate` / `db:migrate` / `db:seed` scripts under `apps/api`. `db:seed` inserts a couple of dummy rows so a fresh DB (and `/health-check`) returns real data. `web` and `api` both depend on `packages/domain` via `workspace:*`. - **`--db postgres` / `--db mysql`** also generate a root `docker-compose.yml` whose credentials match `.env.example`, so `docker compose up -d` gives you a working database. `sqlite` needs nothing extra; `d1` targets Cloudflare Workers (pair with `--deploy cloudflare-workers`). The post-scaffold summary prints the get-started steps for your exact combo (install, dev, and — when a server database is selected — `docker compose up -d` plus the `db:generate` / `db:migrate` commands). +### AI-agent-ready + +Every scaffolded project ships with files that make it work well with Claude Code and other AI agents out of the box: + +- **`AGENTS.md`** (and a `CLAUDE.md` pointing to it) — describes your *exact* stack: real commands, both dev URLs, the monorepo map, the ORM/DB workflow, and the `/health-check` probe. +- **`.claude/settings.json`** — a permission allowlist for routine dev commands (package manager, `docker compose` when a server DB is selected, `wrangler` for Cloudflare, read-only git) so agents don't stall on prompts. +- **`.claude/commands/`** — project slash-commands: `/verify` (typecheck + build + test, and curl the health-check), `/format-fix`, and `/new-component` (follows the repo's naming + JSDoc conventions). + ## Project Structure ``` diff --git a/config/registry.json b/config/registry.json index 42e733e..17e41dd 100644 --- a/config/registry.json +++ b/config/registry.json @@ -595,12 +595,14 @@ "drizzle-orm": "^0.36.4" }, "devDependencies": { - "drizzle-kit": "^0.28.1" + "drizzle-kit": "^0.28.1", + "tsx": "^4.19.2" }, "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "db:seed": "tsx --env-file-if-exists=.env db/seed.ts" } } }, @@ -612,12 +614,14 @@ "@prisma/client": "^5.22.0" }, "devDependencies": { - "prisma": "^5.22.0" + "prisma": "^5.22.0", + "tsx": "^4.19.2" }, "scripts": { "db:generate": "prisma generate", "db:migrate": "prisma migrate dev", - "db:studio": "prisma studio" + "db:studio": "prisma studio", + "db:seed": "tsx --env-file-if-exists=.env prisma/seed.ts" } } } diff --git a/config/templates/orm/drizzle/db/index.ts.tmpl b/config/templates/orm/drizzle/db/index.ts.tmpl index 67b8098..71f792c 100644 --- a/config/templates/orm/drizzle/db/index.ts.tmpl +++ b/config/templates/orm/drizzle/db/index.ts.tmpl @@ -26,6 +26,8 @@ import { drizzle } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; import * as schema from "./schema"; -const sqlite = new Database(process.env.DATABASE_URL ?? "sqlite.db"); +// better-sqlite3 wants a filesystem path, not a `file:` URL — strip the scheme +// so the same DATABASE_URL works for both drizzle-kit and the runtime client. +const sqlite = new Database((process.env.DATABASE_URL ?? "sqlite.db").replace(/^file:/, "")); export const db = drizzle(sqlite, { schema }); {{- end }} diff --git a/config/templates/orm/drizzle/db/seed.ts.tmpl b/config/templates/orm/drizzle/db/seed.ts.tmpl new file mode 100644 index 0000000..f2202a2 --- /dev/null +++ b/config/templates/orm/drizzle/db/seed.ts.tmpl @@ -0,0 +1,20 @@ +{{- if eq (printf "%s" .Database) "d1" -}} +// D1 runs inside the Cloudflare Worker runtime, so there is no module-level +// client to seed from here. Seed with wrangler instead, e.g.: +// wrangler d1 execute --command "INSERT INTO users (name) VALUES ('Ada Lovelace')" +console.log("For D1, seed via `wrangler d1 execute` — see db/seed.ts."); +{{- else -}} +import { db } from "./index"; +import { users } from "./schema"; + +/** Seed the database with a couple of deterministic rows for local dev. */ +async function main() { + await db.insert(users).values([{ name: "Ada Lovelace" }, { name: "Alan Turing" }]); + console.log("Seeded users ✔"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); +{{- end }} diff --git a/config/templates/orm/prisma/prisma/seed.ts.tmpl b/config/templates/orm/prisma/prisma/seed.ts.tmpl new file mode 100644 index 0000000..8597247 --- /dev/null +++ b/config/templates/orm/prisma/prisma/seed.ts.tmpl @@ -0,0 +1,16 @@ +import { db } from "../db"; + +/** Seed the database with a couple of deterministic rows for local dev. */ +async function main() { + await db.user.createMany({ + data: [{ name: "Ada Lovelace" }, { name: "Alan Turing" }], + }); + console.log("Seeded users ✔"); +} + +main() + .catch((err) => { + console.error(err); + process.exit(1); + }) + .finally(() => db.$disconnect()); diff --git a/config/templates/shared/.claude/commands/format-fix.md.tmpl b/config/templates/shared/.claude/commands/format-fix.md.tmpl new file mode 100644 index 0000000..5008d3d --- /dev/null +++ b/config/templates/shared/.claude/commands/format-fix.md.tmpl @@ -0,0 +1,6 @@ +--- +description: Format and lint the codebase, fixing what can be auto-fixed +--- +Run `{{ .PM }} format` to format and lint the codebase. Fix any remaining issues +the formatter/linter can't auto-fix, then re-run until clean. Report what changed +in one or two lines — don't paste the full diff. diff --git a/config/templates/shared/.claude/commands/new-component.md.tmpl b/config/templates/shared/.claude/commands/new-component.md.tmpl new file mode 100644 index 0000000..a248bc2 --- /dev/null +++ b/config/templates/shared/.claude/commands/new-component.md.tmpl @@ -0,0 +1,15 @@ +--- +description: Scaffold a new component following this project's conventions +argument-hint: [what it does] +--- +Create a new {{ .Base }} component for: $ARGUMENTS + +Follow the conventions in `AGENTS.md`: + +- **File name**: `kebab-case`, e.g. `primary-button.*` — use the extension this project already uses for components (`.astro`, `.tsx`, or `.vue`); match the existing files. +- **Location**: pick the right folder under `/components` — `ui/` (primitives), `layout/` (header/footer/nav), `page/` (per-page sections), or `icon/`. +- **JSDoc**: start every component with the JSDoc block from AGENTS.md (Module, @file, @description, @module). +- **Types**: TypeScript `strict`; never `any` (use `unknown`); avoid `as` — prefer type guards. Prefer arrow functions and small, composable components. +- Respect the formatter config; run `{{ .PM }} format` when done. + +Show the created file path and a one-line summary. diff --git a/config/templates/shared/.claude/commands/verify.md.tmpl b/config/templates/shared/.claude/commands/verify.md.tmpl new file mode 100644 index 0000000..5ed659b --- /dev/null +++ b/config/templates/shared/.claude/commands/verify.md.tmpl @@ -0,0 +1,17 @@ +--- +description: Typecheck, build, and test the project; report any failures +--- +Verify the project is healthy. Run, in order, and report results concisely: + +{{ if .Layout.IsMonorepo -}} +1. `{{ .PM }} -r build` — typechecks and builds every workspace (apps/web, apps/api, packages/domain) +2. `{{ .PM }} -r test` — runs tests where a `test` script exists +{{- else -}} +1. `{{ .PM }} build` — typechecks and builds the project +2. `{{ .PM }} test` — runs tests if a `test` script exists +{{- end }} +{{ if ne (printf "%s" .Backend) "none" -}} +3. With the API running (`{{ .PM }} dev`), `curl -s http://localhost:8000/health-check` — confirms the server{{ if ne (printf "%s" .ORM) "none" }} and database connection{{ end }} are up. +{{- end }} + +If anything fails, show the failing output and fix the root cause. Do not report success unless every step passes. diff --git a/config/templates/shared/.claude/settings.json.tmpl b/config/templates/shared/.claude/settings.json.tmpl new file mode 100644 index 0000000..afce82a --- /dev/null +++ b/config/templates/shared/.claude/settings.json.tmpl @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "Bash({{ .PM }} install)", + "Bash({{ .PM }} run:*)", + "Bash({{ .PM }} dev)", + "Bash({{ .PM }} build)", + "Bash({{ .PM }} preview)", + "Bash({{ .PM }} test:*)", + "Bash({{ .PM }} format)", + "Bash({{ .PM }} lint)", +{{- if .Layout.IsMonorepo }} + "Bash({{ .PM }} --filter:*)", + "Bash({{ .PM }} -r:*)", +{{- end }} +{{- if or (eq (printf "%s" .Database) "postgres") (eq (printf "%s" .Database) "mysql") }} + "Bash(docker compose:*)", +{{- end }} +{{- if or (eq (printf "%s" .Deployment) "cloudflare-pages") (eq (printf "%s" .Deployment) "cloudflare-workers") }} + "Bash(wrangler:*)", +{{- end }} + "Bash(git status)", + "Bash(git diff:*)", + "Bash(git log:*)" + ] + } +} diff --git a/config/templates/shared/AGENTS.md.tmpl b/config/templates/shared/AGENTS.md.tmpl index 3ad8f2e..d9399f0 100644 --- a/config/templates/shared/AGENTS.md.tmpl +++ b/config/templates/shared/AGENTS.md.tmpl @@ -1,27 +1,80 @@ # Global AI Agent Guidance ## Overview -This project contains the following stack: +This project was scaffolded with bungkus-cli. Stack: -- Framework: {{ .Base }} +- Framework: {{ .Base }}{{ if ne (printf "%s" .CMS) "none" }} + {{ .CMS }} (CMS){{ end }} - CSS: {{ .CSS }} +- Formatter / Linter: {{ .Fmt }} / {{ .Linter }} - Package Manager: {{ .PM }} +- Layout: {{ .Layout }} +{{- if ne (printf "%s" .Validation) "none" }} +- Validation: {{ .Validation }} +{{- end }} +{{- if ne (printf "%s" .Backend) "none" }} +- Backend: {{ .Backend }} (API on `http://localhost:8000`) +{{- end }} +{{- if ne (printf "%s" .ORM) "none" }} +- ORM / Database: {{ .ORM }} / {{ .Database }} +{{- end }} + +The frontend dev server runs on `http://localhost:3000`. + +## Dev environment tips +- Always use `{{ .PM }}` as the package manager. +- Update `README.md` and `AGENTS.md` when you add a package or a script. +- Don't install packages unless necessary — keep the dependency footprint small. +- Make changes in small chunks; add useful comments for complex logic, not generic ones. + +{{ if .Layout.IsMonorepo -}} +This is a **{{ .PM }} workspace monorepo**: -## Dev environments tips -- Always prioritise to use `{{ .PM }}` as package manager. -- Development PORT is set to `:3000` by default -- Always update README.md and AGENTS.md when new package and new script has been added -- Do not install packages unless nesccessary, practice less dependency -- Prioitise fixing/adding code in smaller chunk. -- Add useful comments to document complex features / code changes, not generic comments +``` +apps/web frontend ({{ .Base }}) +{{ if ne (printf "%s" .Backend) "none" }}apps/api backend ({{ .Backend }}{{ if ne (printf "%s" .ORM) "none" }} + {{ .ORM }}{{ end }}) +{{ end }}packages/domain shared types / schemas (imported by web + api via workspace:*) +``` ```bash -{{ .PM }} install # install dependency -{{ .PM }} dev # start dev server -{{ .PM }} build # production build -{{ .PM }} preview # preview build +{{ .PM }} install # install all workspace deps +{{ .PM }} dev # run every app together{{ if ne (printf "%s" .Backend) "none" }} (web + api){{ end }} +{{ .PM }} -r build # typecheck + build all workspaces +{{ .PM }} --filter web dev # run only the frontend +{{ if ne (printf "%s" .Backend) "none" }}{{ .PM }} --filter api dev # run only the backend +{{ end }}{{ .PM }} format # format and lint +``` +{{- else -}} +```bash +{{ .PM }} install # install dependencies +{{ .PM }} dev # start dev server (http://localhost:3000) +{{ .PM }} build # production build (also typechecks) +{{ .PM }} preview # preview the production build {{ .PM }} format # format and lint code ``` +{{- end }} +{{ if ne (printf "%s" .Backend) "none" }} +## Backend ({{ .Backend }}) + +- The API runs on `http://localhost:8000` (override with the `PORT` env var). +- Start it with `{{ if .Layout.IsMonorepo }}{{ .PM }} dev` (runs web + api together) or `{{ .PM }} --filter api dev`{{ else }}{{ .PM }} run dev:server` (or `{{ .PM }} dev` for the frontend){{ end }}. +- `GET /health-check` returns `{ status: "ok" }`{{ if ne (printf "%s" .ORM) "none" }} plus a read-only list of `users` when the database is reachable — use it to confirm the DB is wired{{ end }}. +- Keep secrets in the API's `.env`; never expose them to the client bundle. +{{- end }} +{{ if ne (printf "%s" .ORM) "none" }} +## Database ({{ .ORM }} / {{ .Database }}) + +{{ if .Layout.IsMonorepo }}Run from `apps/api`, or with `{{ .PM }} --filter api