06 — MODULE SYSTEM
Depends on: 05_UI_UX.md · Next: 07_BLOG_SYSTEM.md
1. Goal
Adding a feature = creating one folder. No editing core routing, nav, permissions, or API index.
2. Module Anatomy
modules/<name>/
├── module.json # Manifest — scanned at build
├── schema.prisma # Prisma fragment (merged into main schema at gen)
├── routes.ts # Route registration (Hono/Next)
├── api.ts # Handlers + Zod validation + services
├── permissions.ts # RBAC: who can read/write/admin
├── menu.ts # Nav item: label, icon, path, plan gate
├── ui/
│ ├── page.tsx # Dashboard page
│ ├── card.tsx # Reusable pieces
│ └── form.tsx
├── docs.md # End-user + developer docs for this module
└── tests/
├── unit.test.ts
└── integration.test.ts
3. Manifest Spec
// modules/links/module.json
{
"name": "links",
"displayName": "Links",
"description": "URL shortening, aliases, expiry, password, QR",
"version": "1.0.0",
"route": "/dashboard/links",
"apiPrefix": "/v1/links",
"icon": "link-2",
"plans": ["free", "starter", "pro", "business", "enterprise"],
"permissions": ["links:read", "links:write"],
"dependencies": [],
"enabled": true
}
Generator reads modules/*/module.json → produces nav.ts, routes.ts, openapi tags, docs sidebar.
4. Tool Subtype
Tools are modules under modules/tools/<tool>/:
modules/tools/password/
├── module.json # { name:"password", displayName:"Password Generator", route:"/tools/password" }
├── page.tsx
├── api.ts # POST /v1/tools/password/generate
└── tests/
Same lifecycle as any module. tools is just a category.
Phase 1 tools (5): links, qr, password (placeholder), utm (placeholder), base64 (placeholder) — only links + qr fully implemented; others scaffolded with module.json + UI shell + 501 API until their phase.
5. API Registration Pattern
// modules/links/routes.ts
import { Hono } from 'hono'
import { createLinkSchema } from './schema'
import { requireAuth } from '#security/guards'
export const linksRoutes = new Hono()
.post('/', requireAuth, async (c) => {
const body = createLinkSchema.parse(await c.req.json())
// ... service
})
.get('/', requireAuth, async (c) => { /* list */ })
Auto-mounted: apps/api/src/index.ts → for (const m of modules) app.route(m.apiPrefix, m.routes).
6. Permission System
// modules/links/permissions.ts
export const permissions = {
"links:read": ["USER", "ADMIN"],
"links:write": ["USER", "ADMIN"],
"links:admin": ["ADMIN"],
}
Global RolesGuard checks permissions.ts per route. UI hides nav items if plan or permission fails (but API enforces — never UI-only).
7. Plans Gate
packages/config/plans.ts is single source:
export const plans = {
free: { limits: { links: 50, clicks: 1000, apiKeys: 1 }, features: ["links", "qr"] },
pro: { limits: { links: 5000, clicks: 100000, apiKeys: 10 }, features: ["links","qr","analytics","teams"] },
}
menu.ts and module.json:plans filter nav; API middleware enforces limits.
8. How to Add a New Module (Copy-Paste Recipe)
cp -r modules/links modules/my-tool
# edit modules/my-tool/module.json (name, route, apiPrefix)
# edit modules/my-tool/api.ts + ui/page.tsx
pnpm gen:modules
pnpm build && pnpm test
No edits to apps/web/app/layout.tsx, apps/api/src/index.ts, or nav.ts by hand.
9. Phase 1 Modules (5 Fully Implemented)
| Module | Status | What ships |
|---|---|---|
links |
✅ Full | All CRUD, expiry, password, alias |
redirect |
✅ Full | GET /:code engine |
qr |
✅ Full | PNG/SVG generation |
analytics |
✅ Full | Overview + per-link |
api-keys + auth |
✅ Full | Sessions + Bearer keys |
Scaffolded but 501: password, utm, base64, teams, billing, admin, bio, blog — have module.json + docs + OpenAPI stub so roadmap is discoverable.
Next: 07_BLOG_SYSTEM.md — content collections.