Skip to content
Y
YAS.SH
Docs

06_MODULE_SYSTEM

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.tsfor (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.

🍪 Cookies & privacy. yas.sh uses only essential cookies to keep you signed in and remember your preferences. We do not run third-party trackers. See our cookie policy and privacy policy.
Settings