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.
.
├─ api/hono/ # backend (Hono): routes, middleware, error envelope
├─ web/next/ # frontend (Next.js): app router, components, content/
└─ packages/
├─ auth/ # Better Auth instance + session types
├─ db/ # Drizzle schema + migrations
├─ env/ # type-safe, validated environment variables
├─ config/ # TS base, tsdown factory, and site.ts (the brand)
└─ scripts/ # build-only tooling (build-time tldts/env derivation); never bundledapi/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, public waitlist), middleware in src/middlewares/ (auth, 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/.
Two shells to build on
(protected)/ (dashboard) and (console)/ (staff, member and above) are auth-gated and near-empty on purpose: they are where your product goes, not finished features. 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
auth, the Better Auth config: optional GitHub/Google OAuth, the organizations + teams plugin, and theadminplugin, which supplies theroleand ban columns while every one of its own endpoints is switched off./consoleis gated by the role ladder insrc/access.ts, a second subpath export (@packages/auth/access) so the web can read the rules without pulling the auth runtime. Exports session types.db: Drizzle schema undersrc/schema/(auth.ts,console.ts,waitlist.ts, grouped by concern rather than one file per table) and generated migrations underdrizzle/.env: every environment variable, validated with @t3-oss/env-core and split per surface (api-hono.ts,auth.ts,db.ts,web-next.ts).config: the TypeScript base every package extends, thetsdownfactory, andsrc/site.ts, the single file a fork edits to rebrand.scripts: build-only Bun tooling (e.g. the tldts breakdown baked intoauth, and the client split signalweb/next'snext.configinlines). It runs during a build and writes to the gitignored repo-root.generated/, never bundled or imported at runtime.
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.