File system
File system
A workspace is a directory whose src/ holds + files. Each + file is a compiler role: its default export is one literal passed to that role’s declaration function from @norbital-ai/bolt. Names come from paths, never from file contents. A file without a + is an ordinary module and may sit anywhere.
The tree
<workspace>/
├── package.json
├── tsconfig.json extends ./.norbital/tsconfig.json
├── norbital.template.json catalogue metadata (templates only)
├── assets/ thumbnail.svg + runtime assets shipped in the artifact
├── seed/ <collection>.json[.gz] rows, assets.json + assets/**, optional seed.ts
├── tests/ bolt test (@effect/vitest + @norbital-ai/bolt/test)
├── .norbital/ generated, never edited
└── src/
├── +workspace.ts tz, locale, currency, app order, AI model classes, connections
├── +secrets.ts secret names: OAuth clients, MCP settings; values set at runtime
├── data/
│ ├── +relationship.ts every relationship
│ ├── model/+<model>.model.ts storage: fields, kinds, constraints, state
│ ├── custom_field/<field>/ +definition.ts, +renderer.svelte
│ └── collection/<model>/ the exposed portion of model <model>
│ ├── +collection.ts reads, writes, the transform, queries, actions, notifications
│ ├── +representation.svelte optional record view (RecordShell otherwise)
│ └── +pipeline.ts optional import / export
├── access_control/
│ ├── team/+team.ts team names → the policies each holds
│ └── policy/+<name>.policy.ts grants, embedded approvals, capabilities, limits
├── integration/<name>/
│ ├── +integration.ts a driver, configuration, credential, feeds, outbound
│ └── +integration.svelte optional setup screen
├── agent/
│ ├── +agent.md in-app agent brief (staff)
│ ├── +agent.external.md brief for external members
│ ├── envoy/+<id>.envoy.ts an agent role a conversation instance deploys
│ ├── skill/+<name>.skill.md an agent skill
│ └── mcp/+<name>.mcp.ts an MCP server the agent may use
├── automation/+<name>.automation.ts cron / event / webhook work and its I/O
├── app/
│ ├── <app>/+app.ts title, icon, audience, pages in nav order
│ ├── <app>/+<page>.page.svelte one page per file
│ └── <group>/+group.ts a nav group; its apps live in its folder
├── i18n/
│ ├── +messages.ts base locale keys
│ └── +<locale>.messages.ts other locales (keys ⊆ the base)
└── lib/ ordinary modules (no roles) The naming rule:
- A folder that already names the thing holds
+<role>.<ext>:data/collection/orders/+collection.ts. - Otherwise the file is
+<name>.<role>.<ext>:access_control/policy/+sales_rep.policy.ts. - Folder names are singular, and names match
^[a-z][a-z0-9_]*$. - An unknown
+file, a role in the wrong folder, or adata/collection/<m>/folder with nodata/model/+<m>.model.tsis a build error whose help prints the role table.
Roles
Each role has one declaration function. collection, app, group, integration and pipeline take their name first; the others take only the literal. Code bodies attach in their own statements after the declaration (c.transform(…), c.query(…), c.action(…), a.run(…)), never inside the literal.
| Role | Path under src/ | Default export | Guide |
|---|---|---|---|
| Workspace settings | +workspace.ts | workspace({…}) | Quick Start |
| Secrets | +secrets.ts | secrets({…}) | Integrations |
| Model | data/model/+<m>.model.ts | model({…}) | Collections |
| Relationships | data/+relationship.ts | relationship({…}) | Collections |
| Custom field | data/custom_field/<f>/+definition.ts | customField({…}) | Collections |
| Custom field renderer | data/custom_field/<f>/+renderer.svelte | { view: CustomFieldView } | UI components |
| Collection | data/collection/<m>/+collection.ts | collection('<m>', {…}) | Write contract |
| Record view | data/collection/<m>/+representation.svelte | { view: RecordView<'<m>'> } | UI components |
| Integration | integration/<n>/+integration.ts | integration('<n>', {…}) | Integrations |
| Integration setup | integration/<n>/+integration.svelte | Svelte component | Integrations |
| Pipeline | data/collection/<m>/+pipeline.ts | pipeline('<m>', {…}) | Pipelines |
| Teams | access_control/team/+team.ts | team({…}) | Policies |
| Policy | access_control/policy/+<p>.policy.ts | policy({…}) | Policies |
| Agent briefs | agent/+agent.md, agent/+agent.external.md | Markdown | Norbius |
| Agent skill | agent/skill/+<s>.skill.md | Markdown with a description: frontmatter | Norbius |
| MCP server | agent/mcp/+<n>.mcp.ts | mcp({…}) | Norbius |
| Envoy | agent/envoy/+<e>.envoy.ts | envoy({…}) | Envoys |
| Automation | automation/+<a>.automation.ts | automation({…}) | Automations |
| App | app/<a>/+app.ts | app('<a>', {…}) | Apps |
| Page | app/<a>/+<p>.page.svelte | Svelte component | Apps |
| Nav group | app/<g>/+group.ts | group('<g>', {…}) | Apps |
| Messages | i18n/+messages.ts, i18n/+<locale>.messages.ts | messages({…}) | Internationalization |
Generated files
.norbital/
├── names.ts, names.client.ts # the names index: every path-derived name → typeof its declaration
├── tsconfig.json # the authored tsconfig.json only extends this
├── representations.check.ts
├── artifact/ # bolt build output
├── seed/ # the base (and sample) seed packs
└── cache/** The compiler writes .norbital/; nothing in it is edited or committed. The names index maps every path-derived name to typeof its declaration, so every other file — every page included — is typed from the declarations. It is rewritten only when a role file is added, renamed or removed.
Commands
bolt check # every error of every stage in one run
bolt build # check + .norbital/artifact/ + seed packs
bolt test # @effect/vitest with TestWorkspace.layer
bolt dev # the author loop: PGlite, live reload, a dev mail sink
bolt start # the self-host server (bolt-server) Five verbs, each exiting 0 when all is well, 1 for an author error and 2 for a tool failure. bolt build is the only gate a host runs.
The manifest is the schema
There are no migration files. A host compares the applied schema with the manifest and applies one plan in one transaction. The plan cannot tell a rename from a drop plus an add, so renaming a field or a model loses its data: add the new field, backfill it, then drop the old one in a later change.
What is not a role
The surface is closed: anything not in the table above does not exist, and these paths are not read — src/collections/, src/datatypes/, src/functions/, src/apps/, src/+agents.md, src/access/policies/, src/capabilities/, +anonymous_limits.ts, vite.config.ts and .norbital/migrations/. Server functions are collection queries and actions, secret names are declared in +secrets.ts, and agent tools are the collections’ reads, queries and actions.