Manifest reference
manifest.json is the whole interface between a pack and the CLI. Point $schema at the published copy and your editor
will check it as you type:
{ "$schema": "https://www.hype-stack.dev/schema/pack-manifest-v1.json" }Identity
| Field | Required | What it does |
|---|---|---|
name | yes | Kebab-case identifier. Match the directory name |
version | yes | Semver. Recorded in the project so a later install can tell versions apart |
displayName | yes | What people see in the picker |
description | yes | One line, shown next to the name |
category | yes | starter, layout, or feature. The catalog supplies this for our own packs, so a community pack has to declare it, and validate fails without it |
tier | schema | free or paid. The published schema requires it, so write free. Nothing in a community pack is licensed, so paid has no effect and validate warns about it |
family | - | Makes the pack a swappable variant of a family (starter-saas, layout, pack-billing). Two packs in one family replace each other |
dependencies | yes | Packs, or family ids, that must be installed first. [] when the pack stands alone |
peerPacks | - | Packs that make this one better if present, but are not required |
adminCompatible | - | Layouts only: whether the admin shell can render this layout |
Files
files is a list of { source, target, strategy }. source is relative to the pack root, target is a path template.
See Build your own pack for the strategies and slots.
dependencies_npm adds npm packages per app:
"dependencies_npm": { "frontend": { "recharts": "^2.15.0" }, "backend": { "zod": "^4.0.0" } }allowBuilds records the verdict for a dependency that runs a build script. pnpm 12 refuses to install one without a
verdict, so the CLI writes yours into the project's pnpm-workspace.yaml:
"allowBuilds": { "sharp": true }What a pack needs from the starter
| Field | For |
|---|---|
provides | Starters only. What the starter gives a project: auth, organizations, permissions, invitations, admin. |
requires | Layouts and feature packs. What the pack needs from the starter, from the same list. |
The installer checks requires against the union of what is installed and what is selected, and refuses the plan by
name when something is missing. A pack that reads the active organization declares organizations and never lands on a
starter without one. The free starter-auth-betterauth provides all five (every account owns a personal workspace, the
admin console is sign-in plus admin management, and the invitation inbox exists without a way to send), so every feature
pack installs on it. Swapping it for a SaaS starter later is an upgrade: compose --packs starter-saas-betterauth
replaces the starter's files, removes what the free one shipped on its own, and registers the admin app again.
Wiring
Copying files is rarely enough. These fields tell the CLI to patch a known file in the project so your code is actually reached. Each one is idempotent, so reinstalling never duplicates an entry.
Backend
| Field | Patches | For |
|---|---|---|
routes | routes/index.ts | Registering a Hono route under a prefix |
sockets | sockets/index.ts | A WebSocket handler |
context | context.ts | A value on the backend request context |
env | env.config.ts, .env.example, .env.test | Environment variables the pack needs |
jobs | src/jobs/index.ts | A scheduled background job (see below) |
queues | src/queues/index.ts | A task queue for slow work (see below) |
enums | packages/enums/src/index.ts | Shared enums both sides import |
permissions | the shared enums package | New permission subjects and role grants |
An env entry names the Zod schema file to merge and the variables to add. Each variable is a plain default, or an
object that tells onboard how to treat it: required prompts, generated rolls a secret at
install, optional is offered once per group.
"env": [
{
"import": "analyticsEnvSchema",
"from": "./analytics.env",
"envExample": {
"section": "config",
"vars": {
"ANALYTICS_WRITE_KEY": { "value": "", "setup": "required" },
"ANALYTICS_FLUSH_MS": "10000"
}
}
}
]A jobs entry names an exported JobDefinition and the file it lives in. The template's scheduler picks it up at boot;
see Scheduled jobs.
"jobs": [{ "import": "analyticsRollupJob", "from": "./features/analytics/jobs/rollup" }]A queues entry names an exported QueueDefinition (built with defineQueue) and the file it lives in, relative to
src/queues/index.ts. The template starts workers for it at boot; see Task queue. It needs
template v1.7.0 or newer, and the install stops with a clear message on an older project.
"queues": [{ "import": "reportExportQueue", "from": "./report-export.queue" }]Frontend
| Field | Patches | For |
|---|---|---|
frontendRoutes | frontend-url/frontend-url.ts | Typing a route your pack adds |
sdkConfigurations | src/api/sdk.ts | HyperFetch client configuration |
nav | constants/nav.constants.ts | A sidebar entry |
navbarActions | constants/navbar-actions.tsx | An icon in the navbar |
userMenuActions | constants/navbar-actions.tsx | An item in the avatar dropdown |
sidebarActions | constants/navbar-actions.tsx | A block above a sidebar layout's navigation |
drawerActions | mobile constants/navbar-actions.tsx | A row at the foot of the mobile drawer |
providers | components/providers/providers.tsx | A React provider mounted at the app root |
settingsTabs | constants/settings-tabs.constants.tsx | A tab on the settings page |
onboardingSteps | setup-organization/steps/index.ts | A step in organization onboarding |
The four action fields are the shell action areas. Each is an exported array the installed layout renders, so a pack
never imports the layout and a layout never imports the pack. The installer records every action in stack.json and
rebuilds the arrays after each install, so it does not matter whether your pack lands before or after the layout. A
drawerActions component receives closeDrawer and owns whatever sheet it opens.
Admin
dashboardRows, orgDetailsRows, orgDetailsCards, and orgDetailsTabs inject components into the admin app: a row
on the dashboard, a row or card on an organization's detail page, or a whole tab next to Overview, Members, and Flags.
They're skipped entirely in projects with no admin app.
Whole-app
postLoginRedirect sets where the app goes after login. shouldInvertLogo flips logo colours in light mode, for marks
drawn on dark backgrounds.
Copy
User-visible strings belong in a message catalog fragment, not in your JSX:
{
"source": "files/frontend/messages/en.json",
"target": "{{frontend.root}}/messages/en.json",
"strategy": "merge-json",
"slot": "frontend"
}Keys are flat snake_case prefixed with your pack's feature (analytics_page_title). Call them as
m.analytics_page_title(). Chrome the codemods generate (nav entries, settings tabs) takes a messageKey next to
label instead of a hardcoded string, so it translates with everything else.
After the install
postInstall is a list of lines printed once the pack lands. Use it for the things you genuinely cannot do for someone:
an API key to create, a webhook to point somewhere, a decision they have to make.
"postInstall": [
"Set ANALYTICS_WRITE_KEY in .env with a key from your provider dashboard",
"Events are batched every 10s; change it in src/features/analytics/config.ts"
]Fields you can ignore
previewNav, preview, previewAdmin, previewMobile, and previewExtension drive the live previews on the Hype
Stack marketplace. They do nothing in a community pack.
Two things are deliberately not fields. There is no migrations: ship .prisma files as ordinary files entries and
let the CLI generate the migration in the project. There is no hook or script field: a pack is data, and nothing in it
executes.