Elysia Architecture
Portable house style for Elysia HTTP apps. Use this skill alone — it does not
depend on other skills.
Job: where code lives, how folders/files are named, and ownership boundaries.
Not this skill: Elysia API quirks, plugin lifecycle details, Eden, adapters,
or latest framework docs. Prefer current Elysia docs for those.
If the target repo already documents architecture (e.g. AGENTS.md) and it
conflicts, repo wins unless the user asks to migrate toward this skill.
Stack defaults (core)
| Piece |
Default |
| Runtime |
Bun (Node/adapters OK if the app already uses them) |
| HTTP |
Elysia feature plugins under src/modules/<feature>/ |
| Module layout |
routes/ + schema/ + domain logic folder |
| Tests |
Vitest under tests/ (unit / integration projects) when the repo uses Vitest |
Domain logic may be plain TypeScript modules. Load matching with-* extensions
when those stacks are present (see below).
Modes
- Scaffold — new feature module from checklist.md + tree.md.
- Apply — place new endpoints/schemas/services in the canonical spots.
- Review — compare the tree to rules.md; propose moves; do not invent a parallel layout.
Hard rules (core)
- Feature =
src/modules/<feature>/. Group by domain (billing, users), not by technical layer at the app root.
- One route file = one exported Elysia plugin with a stable
name, and typically one HTTP verb (or a tight related group). Do not pack unrelated verbs into one file.
routes/index.ts is a mount table only — .use(child) (+ prefix). No handlers, no domain logic, no shared mappers.
- Schemas live under
schema/ (body, response, query, params as needed). Do not dump DTOs into route files or invent parallel hand-written response interfaces when schemas already define the contract.
- Routes stay thin. HTTP concerns (auth guards, status mapping, request/response) in the route file; business rules in services (or plain domain modules).
- No route factories / shared HTTP wrappers. Do not invent
makeSyncGet, mapXToHttp, or utils/*-http.ts that wrap handlers. Inline the edge in each route file.
- Map failures at the HTTP edge only. Public bodies stay small (e.g.
{ error: string } + status). Do not leak internal causes.
- Do not grow
utils/ for HTTP. Prefer schema/, services/domain, or inlined route edges.
- Naming: folder = noun/domain; file = action or aspect (
create.ts, status.ts). Do not repeat the parent folder in the leaf name (billing/billing-status.ts → billing/routes/status.ts).
- Import direction: routes → schema + services; services must not import routes. Cross-feature deps go through services/shared packages, not route→route imports.
- Mount feature trees from the app entry (or a parent feature’s mount table).
Details, anti-patterns, and examples: rules.md, tree.md, examples.md.
Progressive disclosure
| Need |
Read |
| Canonical trees + naming |
references/tree.md |
| Enforce rules + anti-patterns |
references/rules.md |
| Scaffold / review checklists |
references/checklist.md |
| Good vs bad layouts |
references/examples.md |
| Optional stack overlays |
Extensions below |
Extensions
Load an extension only when the matching stack is present (or the user asks).
Extensions add rules; they do not replace the core tree.
| When |
Extension |
| Effect services / Layers / tagged errors |
with-effect.md |
createEnv / packages must not read process.env |
with-env.md |
Session auth mount + { auth: true } macro |
with-session-auth.md |
| Capabilities / grants / identity permissions |
with-capability-authz.md |
| Bearer API keys on a jobs/public API |
with-api-key-auth.md |
| Drizzle schema package + DB in services |
with-drizzle.md |
| OpenAPI derived from route schemas |
with-openapi.md |
| Request logs + OTEL + Effect tracer |
with-observability.md |
| Stable public error body + edge mapping |
with-http-errors.md |
Domain in packages/*, HTTP in app modules |
with-domain-packages.md |
| Shared vs app-local Elysia plugins |
with-shared-plugins.md |
| In-process cron ticks |
with-cron.md |
| Provider webhooks |
with-webhooks.md |
| Internal shared-secret routes |
with-service-auth.md |
Vitest — Elysia handle / what to test |
with-vitest.md |
1---2name: elysia-architecture3description: Enforce portable Elysia app architecture: feature modules, folder trees, naming, one-route-one-file, thin handlers, schema/services ownership, and mount tables. Use when scaffolding a new Elysia API or feature, adding endpoints, reviewing or reorganizing module layout, or when the user asks for elysia-architecture / Elysia house style. Optional with-* overlays for Effect, env, session auth, capability authz, API keys, Drizzle, OpenAPI, observability, HTTP errors, domain packages, plugins, cron, webhooks, service auth, and Vitest.4---56# Elysia Architecture78Portable house style for Elysia HTTP apps. Use this skill alone — it does not9depend on other skills.1011**Job:** where code lives, how folders/files are named, and ownership boundaries.1213**Not this skill:** Elysia API quirks, plugin lifecycle details, Eden, adapters,14or latest framework docs. Prefer current Elysia docs for those.1516If the target repo already documents architecture (e.g. `AGENTS.md`) and it17conflicts, **repo wins** unless the user asks to migrate toward this skill.1819## Stack defaults (core)2021| Piece | Default |22| --- | --- |23| Runtime | Bun (Node/adapters OK if the app already uses them) |24| HTTP | Elysia feature plugins under `src/modules/<feature>/` |25| Module layout | `routes/` + `schema/` + domain logic folder |26| Tests | Vitest under `tests/` (unit / integration projects) when the repo uses Vitest |2728Domain logic may be plain TypeScript modules. Load matching **with-*** extensions29when those stacks are present (see below).3031## Modes32331. **Scaffold** — new feature module from [checklist.md](references/checklist.md) + [tree.md](references/tree.md).342. **Apply** — place new endpoints/schemas/services in the canonical spots.353. **Review** — compare the tree to [rules.md](references/rules.md); propose moves; do not invent a parallel layout.3637## Hard rules (core)38391. **Feature = `src/modules/<feature>/`.** Group by domain (`billing`, `users`), not by technical layer at the app root.402. **One route file = one exported Elysia plugin** with a stable `name`, and typically one HTTP verb (or a tight related group). Do not pack unrelated verbs into one file.413. **`routes/index.ts` is a mount table only** — `.use(child)` (+ `prefix`). No handlers, no domain logic, no shared mappers.424. **Schemas live under `schema/`** (`body`, `response`, `query`, `params` as needed). Do not dump DTOs into route files or invent parallel hand-written response interfaces when schemas already define the contract.435. **Routes stay thin.** HTTP concerns (auth guards, status mapping, request/response) in the route file; business rules in services (or plain domain modules).446. **No route factories / shared HTTP wrappers.** Do not invent `makeSyncGet`, `mapXToHttp`, or `utils/*-http.ts` that wrap handlers. Inline the edge in each route file.457. **Map failures at the HTTP edge only.** Public bodies stay small (e.g. `{ error: string }` + status). Do not leak internal causes.468. **Do not grow `utils/` for HTTP.** Prefer `schema/`, services/domain, or inlined route edges.479. **Naming:** folder = noun/domain; file = action or aspect (`create.ts`, `status.ts`). Do not repeat the parent folder in the leaf name (`billing/billing-status.ts` → `billing/routes/status.ts`).4810. **Import direction:** routes → schema + services; services must not import routes. Cross-feature deps go through services/shared packages, not route→route imports.4911. **Mount** feature trees from the app entry (or a parent feature’s mount table).5051Details, anti-patterns, and examples: [rules.md](references/rules.md), [tree.md](references/tree.md), [examples.md](references/examples.md).5253## Progressive disclosure5455| Need | Read |56| --- | --- |57| Canonical trees + naming | [references/tree.md](references/tree.md) |58| Enforce rules + anti-patterns | [references/rules.md](references/rules.md) |59| Scaffold / review checklists | [references/checklist.md](references/checklist.md) |60| Good vs bad layouts | [references/examples.md](references/examples.md) |61| Optional stack overlays | [Extensions](#extensions) below |6263## Extensions6465Load an extension **only** when the matching stack is present (or the user asks).66Extensions add rules; they do not replace the core tree.6768| When | Extension |69| --- | --- |70| Effect services / Layers / tagged errors | [with-effect.md](references/with-effect.md) |71| `createEnv` / packages must not read `process.env` | [with-env.md](references/with-env.md) |72| Session auth mount + `{ auth: true }` macro | [with-session-auth.md](references/with-session-auth.md) |73| Capabilities / grants / identity permissions | [with-capability-authz.md](references/with-capability-authz.md) |74| Bearer API keys on a jobs/public API | [with-api-key-auth.md](references/with-api-key-auth.md) |75| Drizzle schema package + DB in services | [with-drizzle.md](references/with-drizzle.md) |76| OpenAPI derived from route schemas | [with-openapi.md](references/with-openapi.md) |77| Request logs + OTEL + Effect tracer | [with-observability.md](references/with-observability.md) |78| Stable public error body + edge mapping | [with-http-errors.md](references/with-http-errors.md) |79| Domain in `packages/*`, HTTP in app modules | [with-domain-packages.md](references/with-domain-packages.md) |80| Shared vs app-local Elysia plugins | [with-shared-plugins.md](references/with-shared-plugins.md) |81| In-process cron ticks | [with-cron.md](references/with-cron.md) |82| Provider webhooks | [with-webhooks.md](references/with-webhooks.md) |83| Internal shared-secret routes | [with-service-auth.md](references/with-service-auth.md) |84| Vitest — Elysia `handle` / what to test | [with-vitest.md](references/with-vitest.md) |