ZeroStarter

Project Structure

How the monorepo fits together: two apps, their packages, one import graph.

The whole repo is one mental model: two deployable apps, their packages, and a single import graph that ties them together.

  • web/next: the Next.js frontend (App Router, React 19).
  • api/hono: the Hono backend that owns every route and talks to the database.
  • packages/*, the shared code plus build tooling: the auth instance, the Drizzle schema, validated env, the brand config, and a build-only scripts package.

Three import aliases wire it together: @api/hono gives the frontend its API types, @packages/* pulls in a shared package, and @/ is the local-to-this-app alias. Imports only point down that list: apps depend on packages, never the reverse.

.
├─ .agents/skills/  # the agent skills (.claude/skills and .github/skills symlink here)
├─ api/hono/        # backend (Hono): routes, middleware, error envelope
├─ web/next/        # frontend (Next.js): app router, components, content/
├─ packages/
│  ├─ auth/         # Better Auth instance + session types
│  ├─ cli/          # the zerostarter CLI (canonical repo only; a fork does not take it)
│  ├─ config/       # TS base, tsdown factory, and site.ts (the brand)
│  ├─ db/           # Drizzle schema + migrations
│  ├─ env/          # type-safe, validated environment variables
│  └─ scripts/      # build tooling, auth schema regenerator; never bundled
├─ tests/           # the whole suite, mirroring each subject's path (canonical repo only)
└─ AGENTS.md        # the rules every agent reads first (CLAUDE.md symlinks to it)

What a fork does and does not take is one file, the root .gitpickignore; see the fork boundary.

api/hono

The entry point is src/index.ts, which exports the AppType the frontend infers from. Routes live in src/routers/ (auth, the local-only agents sign-in, protected v1 with the console-only admin mounted under it, public waitlist), middleware in src/middlewares/ (auth, console access, feature gating, rate limiting), and src/lib/error.ts holds the onError switchboard that shapes every failure into the { error: { code, message } } envelope.

web/next

Standard App Router, grouped by concern: (content)/ for docs and blog, (protected)/ for the signed-in dashboard, (console)/ for the staff console, and (llms.txt)/ for the generated AI endpoints. The typed API client is one file (src/lib/api/client.ts), and MDX content lives under content/docs/ and content/blog/.

A shell and a set of worked examples

(protected)/ (dashboard) and (console)/ (staff, member and above) are both auth-gated. The dashboard is near-empty on purpose: it is where your product goes, not a finished feature. The console ships its Access section as the pattern to copy, (access)/users/ and (access)/allowlist/, plus read-only activity/ and waitlist/, each a data table with its column defs and fetch wiring colocated in a components/ folder beside the page.

Marketing routes

The (marketing)/ route group under src/app/ holds the marketing surfaces: the landing page (/) plus the author's own /hire and /resume. They stay in the canonical repo but zerostarter init strips the whole group and writes a fresh app/page.tsx in its place. A direct clone that skips init can delete web/next/src/app/(marketing), add its own app/page.tsx, and remove the "Hire" link in web/next/src/components/common/navbar.tsx.

src/components/ is grouped by domain the same way, one folder per area, so a component lives next to the surface it serves: common/ (shared pieces like the navbar), shell/ (the app frame: PageShell, PageHeader, and the sidebar-* parts), dashboard/, console/, docs/, blog/, marketing/, and ui/. Cross-domain families that follow the shadcn single-module pattern sit at the top level as one file, like data-table.tsx (see Data Tables). ui/ is the generated shadcn registry: don't hand-edit it (the sync rewrites it); customize through the post-sync script instead.

The packages

PackageHolds
authThe Better Auth config and the console's access rules. Exports session types
cliThe zerostarter npm CLI: init, reinit, sync. Canonical repo only; a fork does not take it
configThe TypeScript base every package extends, the tsdown factory, and src/site.ts, the one file a fork edits to rebrand
dbThe Drizzle schema under src/schema/ and the generated migrations under drizzle/
envEvery environment variable, validated with @t3-oss/env-core and split per consumer
scriptsBun build tooling. Never bundled, never imported at runtime

Some have a detail worth knowing before you edit:

  • auth declares its plugins and any extra column in src/schema.ts: optional GitHub/Google OAuth, organizations + teams, and the admin plugin, which supplies the role and ban columns while every one of its own endpoints is switched off. packages/db/src/schema/auth.ts is generated from that file (bun run auth:schema). The role ladder that gates /console lives in src/access.ts, a second subpath export (@packages/auth/access), so the web can read the rules without pulling the auth runtime.
  • db groups its schema by concern rather than one file per table: auth.ts, console.ts, waitlist.ts. See Database.
  • env has one slice per consumer: api-hono.ts, auth.ts, db.ts, web-next.ts. See Environment Variables.

scripts holds build-time scripts and on-demand ones. The build-time ones write to the gitignored repo-root .generated/: the tldts breakdown baked into auth, the client split signal web/next's next.config inlines, and the data-table font metrics. The on-demand ones write committed source or decide something: auth-schema (bun run auth:schema) writes packages/db/src/schema/auth.ts, and release-version decides the number a release window has earned.

Bun catalog

The root package.json defines a catalog: block with one version per shared dependency; every workspace references those deps as catalog: instead of a literal version, so the whole monorepo stays on one version of each. .github/scripts/deps-manager.ts normalizes and sorts these entries on postinstall and whenever a package.json is staged.

Next