The project descriptor
Understand the one JSON document that defines an Alvo backend: what belongs in it, where it lives, how it is checked and how it changes over time.
An Alvo backend is defined by one JSON document, the project descriptor. It says what the backend is: its entities and fields, who may read and write each one, what happens as a row is written, which values are derived, and who may manage the project. Alvo turns it into tables, a REST API with its own OpenAPI document, compiled access rules and hooks. There is no second place where any of that is configured.
This is the whole help-desk backend the tutorial builds:
{ "$schema": "https://alvo.dev/schema/v1/project.json", "apiVersion": "alvo.dev/v1", "name": "help-desk", "auth": { "providers": ["local"], "roles": ["admin", "agent"] }, "entities": { "tickets": { "audit": true, "fields": { "title": { "type": "string", "required": true, "maxLength": 120 }, "body": { "type": "text" }, "priority": { "type": "enum", "values": ["low", "normal", "high"], "default": "normal" }, "status": { "type": "enum", "values": ["open", "closed"], "default": "open" }, "estimate_hours": { "type": "decimal", "precision": 5, "scale": 1 }, "hourly_rate": { "type": "decimal", "precision": 6, "scale": 2 }, "estimate_cost": { "type": "decimal", "precision": 12, "scale": 2, "computed": "estimate_hours * hourly_rate" } }, "rules": { "list": "'authenticated' in @user.roles", "get": "'authenticated' in @user.roles", "create": "'agent' in @user.roles || 'admin' in @user.roles", "update": "'agent' in @user.roles || 'admin' in @user.roles", "delete": "'admin' in @user.roles" }, "hooks": { "beforeCreate": [ { "action": { "mutate": { "title": { "$cel": "trim(new.title)" } } } }, { "condition": "new.priority == 'high' && !has(new.body)", "action": { "reject": "A high-priority ticket needs a body." } } ] } } }}What belongs in it, and what does not
Section titled “What belongs in it, and what does not”The descriptor defines the backend. Infrastructure stays out: connection strings, API keys, the administrator’s credentials, the AI connection, the startup mode and the path the host is served under are configuration of the deployment (Configuration keys). The line is deliberate: a descriptor can live in Git and be reviewed like code without carrying a secret, and the same descriptor runs unchanged against a laptop’s SQLite file and a production PostgreSQL.
Access rules sit on the other side of that line: who may manage the project is the descriptor’s access block, so it is
versioned and reviewed with everything else.
One descriptor, two homes
Section titled “One descriptor, two homes”The format is one; where the current version lives has two answers.
- A file. The standalone image reads
/alvo/descriptor.json; an embedded host passes a path toFromDescriptor. The file can live in your repository, and changing it plus a restart is a deploy. - Records in the database. Every apply, on boot or through the Management API or the dashboard, appends a revision to the project’s history in the database: the descriptor, who applied it, when and why. That history is append-only, and it is what the dashboard reads and rolls back.
The two meet at every boot. The host compares the file with the database: a descriptor the history already holds at an older revision is not served, so a stale file can never quietly undo a change made through the API. The dashboard’s Import / export moves a descriptor between the two homes byte for byte. Which home is authoritative is your choice; Apply and evolve your descriptor shows both loops.
The top-level blocks
Section titled “The top-level blocks”Only apiVersion, name and entities are required. Each block has its own reference page, generated from the
schema:
| Block | What it defines | In this build |
|---|---|---|
entities | entities with their fields, rules, hooks, computed fields and rollups and indexes | honoured, except the parts listed as refused |
auth | the application’s roles, and the sign-in providers | honoured; sign-in providers other than local are not run |
tenancy | whether the backend is multi-tenant | honoured |
access | which roles reach the viewer, developer and admin management levels | honoured |
formats | named validation formats a field can reference | honoured |
templates, webhooks | message templates and webhook endpoints an after-hook uses | run when an after-hook uses them; the rest is not |
automation, functions, dynamicEntities | event rules, custom functions, runtime entities | parsed, not run |
branding | the project’s display name and logo | parsed, nothing renders it yet |
$schema, description and revision are metadata, and keys starting with x- are carried through apply and export
untouched, for your own tooling. Capabilities in this build is the authority on
the last column.
How a descriptor is checked
Section titled “How a descriptor is checked”A descriptor is checked in layers, and every finding names the place with a JSON pointer and says how to fix it. All of it happens when the descriptor is applied, never when a request arrives: a rule naming a column that does not exist fails the apply, not the first caller.
- The JSON Schema. The shape: required keys, types, names, the facets each field type must carry. The schema closes
every object (
additionalProperties: false, with onlyx-extension keys admitted), so a misspelled key is an error, not a silently ignored one. - The semantic checks. What the schema cannot express: references to entities and fields that exist, reserved and
framework-managed names, roles a rule names that
auth.rolesdoes not declare. Every rule, hook and computed expression is compiled in its CEL profile against the entity it belongs to. - The apply. Features this build accepts in the schema but cannot honour are refused with a reason, rather than accepted and ignored. Blocks it parses and does not run are warned. Then the migration is planned against the current schema, and a plan that would discard data is refused unless the apply allows it explicitly.
The same checks answer a dry run (?dryRun=true on the Management API), the dashboard’s live check on each expression,
and the schema assistant’s proposals. Nothing applies half a descriptor.
Revisions
Section titled “Revisions”Every accepted apply appends a revision: the descriptor, its number, the author and the reason. A change through the
Management API names the revision it was written against in If-Match; if someone applied first, the change is refused
rather than written over theirs. A rollback applies an earlier revision’s descriptor as a new revision; nothing is
erased. Apply and evolve your descriptor walks through it, including renames that
keep their data and drops that need your permission.
Format version and editor support
Section titled “Format version and editor support”apiVersion is alvo.dev/v1. The schema’s own rule for it: within v1 the format only grows, and a breaking change
becomes alvo.dev/v2. The schema has a description on every key, and this site serves it at
https://alvo.burgyn.online/schema/v1/project.json. The $schema URL a descriptor carries,
https://alvo.dev/schema/v1/project.json, does not resolve yet, so map your editor to the served copy:
For coding agents shows the VS Code setting.
Put it to work
Section titled “Put it to work”- Tutorial: your first backend: write a descriptor step by step.
- Entities and fields: the core of every descriptor.
- Apply and evolve your descriptor: change a running backend safely.
- Descriptor reference: every key, generated from the schema.