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)
| Layer | Location | Rule |
|---|---|---|
| Backend | apps/api (Hono, port 4000) | All API routes, auth server, DB access. Secrets env only here. |
| Frontend | apps/web (Next.js, port 1212) | Pure UI. No DB access, no Better Auth server instance, no secrets. |
| Database | packages/db | Drizzle schema + pooled client (@workspace/db). Never create local db.ts. |
| Shared UI | packages/ui | Reusable 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 theroutesmarker inapps/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 inapps/web/next.config.ts.
Workflow
- Plan — Read
features.json(installed features) and manifests inpackages/feature-manager/features/*/feature.json. Deciderequires, DB tables, env vars, dependencies, dashboard pages. Ask the user for anything unclear. - Scaffold — Create
packages/feature-manager/features/<name>/with the directory layout below and a valid starterfeature.json. - Author — Write template files per target (
api/,web/,ui/,docs/) and one snippet file per insertion. - Self-check — For every manifest entry verify:
copies[].fromexists; everyinsertions[].snippetexists; everyinsertions[].fileexists in the repo and contains the referenced marker region (feature-manager:<marker>:begin);requireslists only installed features; no orphan files in the feature dir.pnpm add-feature <name> --dry-run --strictperforms these checks for you and prints the full plan without writing anything. - Install — Run
pnpm add-feature <name>from the repo root, thenpnpm install, env vars, and the printedpostInstallcommands. - Verify —
pnpm verify-features <name>should be clean;pnpm typecheckat root; confirm docs page and README entry.
Directory layout
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
{ "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/envare 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.postInstallcommands 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). requiresmust list features already present infeatures.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:
| File | Marker | Snippet shape |
|---|---|---|
apps/api/src/app.ts | imports | import { xRoutes } from "./features/<name>/routes" |
apps/api/src/app.ts | routes | .route("/", xRoutes) |
packages/db/src/schema.ts | tables | Drizzle pgTable(...) definitions |
apps/web/components/dashboard/sidebar.tsx | imports | Icon imports, e.g. import { NoteBlank } from "lucide-react" |
apps/web/components/dashboard/sidebar.tsx | nav | { label: "...", href: "/dashboard/...", icon: Icon }, |
apps/web/content/docs/index.mdx | cards | <Card title="..." href="/docs/<name>" description="..." /> |
README.md | features | - **<name>** — One-line description |
External-app features. The
cms,media-r2, andunsplashpackages 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: theirapps/celestiatargets only exist once that app lives atapps/celestia. Do not point new features atapps/celestia— follow theapps/api/apps/websplit above.
Writing documentation for a feature
- Author
docs/<name>.mdxinsidepackages/feature-manager/features/<name>/. - Copy it via
copiestoapps/web/content/docs/<name>.mdx. jsonAppendsentry forapps/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.insertionsentry for thecardsmarker inapps/web/content/docs/index.mdx.insertionsentry for thefeaturesmarker inREADME.md.
CLI reference
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 assistantFlags
| Flag | Applies to | Effect |
|---|---|---|
-n, --dry-run | add, remove | Print every planned action and write nothing |
-f, --force | add | Reinstall or upgrade an already-installed feature |
-f, --force | remove | Replace or delete files that were modified after install |
--strict | add, remove | Turn warnings (missing markers, version conflicts) into errors |
-j, --json | all | Emit a machine-readable report instead of prose |
-q, --quiet | all | Only print warnings, errors and the final result |
-h, --help | all | Command 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--forceis used. - Precise dependency tracking. The tracker records which packages an install
added versus merely re-ranged, so
removedeletes 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
:beginsentinel.
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
jsonAppendsentries 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> --forceupgrades it)
It exits non-zero when it finds an error, so it can gate CI.