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
| Package | Holds |
|---|---|
auth | The Better Auth config and the console's access rules. Exports session types |
cli | The zerostarter npm CLI: init, reinit, sync. Canonical repo only; a fork does not take it |
config | The TypeScript base every package extends, the tsdown factory, and src/site.ts, the one file a fork edits to rebrand |
db | The Drizzle schema under src/schema/ and the generated migrations under drizzle/ |
env | Every environment variable, validated with @t3-oss/env-core and split per consumer |
scripts | Bun build tooling. Never bundled, never imported at runtime |
Some have a detail worth knowing before you edit:
authdeclares its plugins and any extra column insrc/schema.ts: optional GitHub/Google OAuth, organizations + teams, and theadminplugin, which supplies theroleand ban columns while every one of its own endpoints is switched off.packages/db/src/schema/auth.tsis generated from that file (bun run auth:schema). The role ladder that gates/consolelives insrc/access.ts, a second subpath export (@packages/auth/access), so the web can read the rules without pulling the auth runtime.dbgroups its schema by concern rather than one file per table:auth.ts,console.ts,waitlist.ts. See Database.envhas 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
- Architecture: why the graph is shaped this way.
- The Type-Safe API: how
AppTypecrosses from api to web.