Creating Features

How to scaffold, author, and install feature packages for the Celestia feature plugin system

Creating Features

New features MUST be authored as installable feature packages under packages/feature-manager/features/<name>/ and installed with the feature-manager CLI — never hand-copy files into apps/ or packages/ while authoring. The feature-manager add command performs the actual copies and insertions, which keeps features installable, removable, and portable.

Use packages/feature-manager/features/blog/ as the canonical reference implementation.

Architecture constraints (hard rules)

LayerLocationRule
Backendapps/api (Hono, port 4000)All API routes, auth server, DB access. Secrets env only here.
Frontendapps/web (Next.js, port 1212)Pure UI. No DB access, no Better Auth server instance, no secrets.
Databasepackages/dbDrizzle schema + pooled client (@workspace/db). Never create local db.ts.
Shared UIpackages/uiReusable shadcn/ui components (@celestia-project/ui).
  • Backend code must never import from apps/web; frontend code must never import server auth or DB directly.
  • API routes live in apps/api/src/features/<name>/routes.ts, mounted via the routes marker in apps/api/src/app.ts. Use the /api/... prefix; never collide with the reserved /api/auth/* catch-all.
  • The frontend reaches the API through the /api/* rewrite proxy in apps/web/next.config.ts.

Workflow

  1. Plan — Read features.json (installed features) and manifests in packages/feature-manager/features/*/feature.json. Decide requires, DB tables, env vars, dependencies, dashboard pages. Ask the user for anything unclear.
  2. Scaffold — Create packages/feature-manager/features/<name>/ with the directory layout below and a valid starter feature.json.
  3. Author — Write template files per target (api/, web/, ui/, docs/) and one snippet file per insertion.
  4. Self-check — For every manifest entry verify: copies[].from exists; every insertions[].snippet exists; every insertions[].file exists in the repo and contains the referenced marker region (feature-manager:<marker>:begin); requires lists only installed features; no orphan files in the feature dir. pnpm add-feature <name> --dry-run --strict performs these checks for you and prints the full plan without writing anything.
  5. Install — Run pnpm add-feature <name> from the repo root, then pnpm install, env vars, and the printed postInstall commands.
  6. Verify — pnpm verify-features <name> should be clean; pnpm typecheck at root; confirm docs page and README entry.

Directory layout

packages/feature-manager/features/<name>/Directory Structure
packages/feature-manager/features/<name>/
├── api/          → files copied into apps/api  (e.g. api/src/features/<name>/routes.ts)
├── web/          → files copied into apps/web  (pages, components, hooks)
├── ui/           → files copied into packages/ui
├── docs/         → files copied into apps/web/content/docs/
├── snippets/     → insertion snippets for marker-based edits
└── feature.json  → manifest (copies / insertions format — never the legacy `files` map)

Paths in copies[].from and insertions[].snippet are relative to the feature dir; copies[].to, insertions[].file, and jsonAppends[].file are relative to the repo root.

feature.json schema

json
{  "name": "<name>",  "version": "1.0.0",  "description": "One-line summary shown by `feature-manager list`",  "requires": ["auth"],  "copies": [    { "from": "api/src/features/<name>/routes.ts", "to": "apps/api/src/features/<name>/routes.ts" },    { "from": "docs/<name>.mdx", "to": "apps/web/content/docs/<name>.mdx" }  ],  "insertions": [    { "file": "apps/api/src/app.ts", "marker": "imports", "snippet": "snippets/api-imports.ts" },    { "file": "apps/api/src/app.ts", "marker": "routes", "snippet": "snippets/api-routes.ts" }  ],  "jsonAppends": [    { "file": "apps/web/content/docs/meta.json", "path": "pages", "value": "<name>" }  ],  "dependencies": {    "apps/api": { "some-lib": "^1.0.0" }  },  "devDependencies": {},  "env": {    "apps/api": ["SOME_KEY=change-me"]  },  "postInstall": ["pnpm --filter @workspace/db db:push"],  "notes": "Short human summary printed after install."}
  • dependencies / devDependencies / env are keyed by target package dir (apps/api, apps/web, packages/ui, ...).
  • Include postInstall: ["pnpm --filter @workspace/db db:push"] whenever the feature adds DB tables. postInstall commands are not run automatically: the CLI prints them as next steps, and runs them for you only when you pass --yes (e.g. pnpm add-feature <name> --yes).
  • requires must list features already present in features.json.

Available marker regions

Snippets are inserted between sentinel comments; feature-manager wraps each snippet in a per-feature block automatically. The comment style is chosen from the file extension: // for .ts/.tsx/.js, # for .yml/.toml/.sh/.py, <!-- --> for .md/.html, {/* */} for .mdx, and /* */ for .css/.scss. JSON files cannot hold comments — use jsonAppends for those. Only use regions that already exist in the target file — verify before referencing them (a missing region is reported as a warning with the list of markers that file does have).

Current regions:

FileMarkerSnippet shape
apps/api/src/app.tsimportsimport { xRoutes } from "./features/<name>/routes"
apps/api/src/app.tsroutes.route("/", xRoutes)
packages/db/src/schema.tstablesDrizzle pgTable(...) definitions
apps/web/components/dashboard/sidebar.tsximportsIcon imports, e.g. import { NoteBlank } from "lucide-react"
apps/web/components/dashboard/sidebar.tsxnav{ label: "...", href: "/dashboard/...", icon: Icon },
apps/web/content/docs/index.mdxcards<Card title="..." href="/docs/<name>" description="..." />
README.mdfeatures- **<name>** — One-line description

External-app features. The cms, media-r2, and unsplash packages target the Celestia app (apps/celestia/...), which is developed outside this monorepo. They are kept here as installable templates but are not installed in this starter: their apps/celestia targets only exist once that app lives at apps/celestia. Do not point new features at apps/celestia — follow the apps/api / apps/web split above.

Writing documentation for a feature

  1. Author docs/<name>.mdx inside packages/feature-manager/features/<name>/.
  2. Copy it via copies to apps/web/content/docs/<name>.mdx.
  3. jsonAppends entry for apps/web/content/docs/meta.json → pages. The append lands at the end of the array, which is the end of the last sidebar section (---Features---) — exactly where installed feature docs belong. The sidebar's groups are the ---Section--- separator strings in that file.
  4. insertions entry for the cards marker in apps/web/content/docs/index.mdx.
  5. insertions entry for the features marker in README.md.

CLI reference

bash
pnpm add-feature <name>       # install packages/feature-manager/features/<name> into the workspacepnpm remove-feature <name>    # uninstall cleanly (removes copies + marker blocks)pnpm list-features            # show available/installed featurespnpm verify-features [name]   # check installed features for drift or breakagepnpm prompt-feature <name>    # write .prompts/verify-<name>.md for an AI assistant

Flags

FlagApplies toEffect
-n, --dry-runadd, removePrint every planned action and write nothing
-f, --forceaddReinstall or upgrade an already-installed feature
-f, --forceremoveReplace or delete files that were modified after install
--strictadd, removeTurn warnings (missing markers, version conflicts) into errors
-j, --jsonallEmit a machine-readable report instead of prose
-q, --quietallOnly print warnings, errors and the final result
-h, --helpallCommand help (feature-manager help add)

Exit codes: 0 success, 1 operation failed, 2 usage error.

Safety guarantees

  • Atomic installs. Every path an install will touch is snapshotted first. If any step throws, the whole operation is rolled back — a failed install never leaves a half-applied repository.
  • Validated manifests. Malformed fields, invalid marker names, duplicate targets and paths that escape the repository (../…, absolute paths) are rejected before anything is written.
  • Shared files. When two features copy to the same path, the newest install wins and the previous content is backed up under .feature-manager/backups/. Removing a feature restores the previous owner's version instead of deleting a file another feature still needs. Files you edited after install are kept unless --force is used.
  • Precise dependency tracking. The tracker records which packages an install added versus merely re-ranged, so remove deletes only what it introduced and restores the ranges it changed.
  • Idempotent markers. Re-installing a feature replaces its own marker block in place — never duplicating it — and repairs an orphaned :begin sentinel.

Verifying an install

pnpm verify-features is read-only and reports, per installed feature:

  • copied files that are missing or were modified after install
  • marker regions or snippet blocks that disappeared
  • jsonAppends entries and dependencies that are no longer present
  • environment variables the feature needs that are not in .env / .env.example
  • prerequisites that are no longer installed
  • version drift — the feature directory is newer than what is installed (pnpm add-feature <name> --force upgrades it)

It exits non-zero when it finds an error, so it can gate CI.