Architecture
How the darna-stack monorepo fits together.
A pnpm monorepo with three apps. Each one ships independently to Cloudflare Workers. Auth crosses every boundary — WorkOS issues access tokens, the backend verifies them.
Layout
darna-stack/
├── apps/
│ ├── backend/ Hono + Effect API on :4000
│ ├── admin/ Next.js 16 admin panel on :3000
│ └── docs/ Fumadocs site on :3001 (this app)
├── infra/ deploy notes, env templates
├── package.json workspace root
└── pnpm-workspace.yamlRequest flow
┌─────────────────┐
│ WorkOS │ hosted sign-in, JWKS
└────┬───────┬────┘
OAuth │ │ JWT verify
callback ▼ ▼
┌──────────────┐ ┌──────────────┐
│ admin │ │ backend │
│ :3000 │──▶ :4000 │ Bearer <accessToken>
│ Next 16 │ │ Hono+Effect │
│ AuthKit │ │ hono-openapi│
└──────────────┘ └──────────────┘
│
▼
/openapi /docsThe admin obtains a WorkOS access token via AuthKit, forwards it as
Authorization: Bearer … to the backend, and the backend's adminMiddleware
verifies the JWT against the WorkOS JWKS for the same WORKOS_CLIENT_ID.
Apps
Backend
Hono routes, Effect services, OpenAPI spec, Scalar docs viewer, adminMiddleware.
Admin
Next.js 16 with WorkOS AuthKit. Edge middleware enforces auth on every route.
Auth
The WorkOS handshake end-to-end. What lives where.
Deploy
Cloudflare Workers via wrangler (backend) and OpenNext (Next.js apps).
Conventions
- Package names —
@darna/backend,@darna/admin,@darna/docs. Workspace filters use these (pnpm --filter @darna/backend …). - Ports — backend
4000, admin3000, docs3001. Hardcoded in each app'sdevscript sopnpm dev(parallel) doesn't collide. - Wrangler — every deployable app has its own
wrangler.jsonc.compatibility_dateis the same across all three. - Env files —
.env(Node dev),.env.local(Next.js dev),.dev.vars(wrangler dev), all gitignored. Each app has a.examplenext to it. Production secrets go throughwrangler secret put. - Effect runtime — backend code provides services through one
ManagedRuntime(src/lib/effect/runtime.ts). HTTP handlers go throughrunRoute(span, c, effect)which adds an OTel span and rendersRenderableErrorfailures.
Every change to a Next.js app needs turbopack.root pinned to the monorepo
root — pnpm's symlink farm under node_modules/.pnpm lives outside the app
directory, and Turbopack refuses to compile anything outside its root. See
Deploy for the exact config.
Top-level scripts
| Command | What it does |
|---|---|
pnpm dev | All three apps in parallel |
pnpm dev:backend / dev:admin / dev:docs | One app at a time |
pnpm typecheck | Recursive tsc --noEmit |
pnpm deploy | Backend → admin → docs to Cloudflare Workers |
pnpm deps:check | pnpm outdated -r |
pnpm deps:update | Interactive pnpm update -r -i --latest |