Manifest reference
xeer.app.json is the whole shape of your app in one file. xeer check validates it and reports any
problem with a file, a line, and a suggested repair.
Unknown keys are errors, everywhere. Every object in this document is closed, so a misspelled field name is a failed build rather than a silently ignored setting. That is what makes the manifest trustworthy as a description of an app.
{
"$schema": "https://spec.xeer.dev/application-v0.schema.json",
"format": "xeer.application-source.v0",
"name": "team-board",
"entrypoints": { "client": "src/client.tsx", "server": "src/server.ts" },
"app": {
"spa": true,
"title": "Team Board",
"language": "en",
"description": "A shared board for a small team.",
"favicon": "/favicon.svg"
},
"database": {
"version": 1,
"tables": {
"cards": {
"fields": {
"title": { "type": "string", "maxLength": 200 },
"status": { "type": "string", "maxLength": 32 },
"workspaceId": { "type": "string", "maxLength": 96 },
"dueAt": { "type": "datetime", "optional": true }
},
"indexes": {
"by_workspace": ["workspaceId"],
"by_workspace_status": ["workspaceId", "status"]
}
}
}
},
"capabilities": ["database"],
"budgets": { "queryRows": 500, "liveConnections": 200 }
}#Top level
| Field | Required | Rule |
|---|---|---|
format | yes | Exactly "xeer.application-source.v0". |
name | yes | ^[a-z][a-z0-9-]{1,62}$ — a lowercase slug, 2 to 63 characters, starting with a letter. |
entrypoints | yes | { client, server }, both required. |
$schema | no | Any valid URL. An editor hint; it is not fetched and does not affect validation. |
app | no | Document metadata. Below. |
database | no | Tables and indexes. Below. |
storage | no | Object-store limits. Below. |
capabilities | no | Declared powers. Below. |
budgets | no | Resource ceilings. Below. |
There is no version, runtime, env, auth, routes, or assets field. Routes are declared in your
server code; environment values live outside the repository; assets are whatever is in
public/.
#entrypoints
"entrypoints": { "client": "src/client.tsx", "server": "src/server.ts" }Paths are relative to the manifest and must use forward slashes. Each must exist, must be a file, and must
resolve inside the project root — a path that escapes it, including by symlink, is refused
(XE1101). See the two zones for what
each side may import.
#app
All optional. Controls the generated HTML document.
| Field | Type | Default | Rule |
|---|---|---|---|
spa | boolean | true | Serve the app shell for unmatched paths. |
title | string | the app's name | 1–120 characters, no leading or trailing whitespace, no control characters. |
language | string | "en" | A constrained BCP 47 tag: ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$, up to 35 characters. |
description | string | "" | Up to 300 characters, no control characters. |
favicon | string | none | A root-relative path into public/, 2–256 characters, starting with /. No //, no . or .. segments. |
#database
Present when — and only when — capabilities includes "database". See
Capabilities.
| Field | Required | Rule |
|---|---|---|
tables | yes | A record of table name to table definition. |
version | no | A positive integer, default 1. Sequences schema changes; excluded from the structural schema hash. |
#Tables
"cards": {
"fields": { "title": { "type": "string", "maxLength": 200 } },
"indexes": { "by_title": ["title"] }
}Table names, field names, index names, and the field names inside an index are all identifiers:
^[A-Za-z][A-Za-z0-9_]*$. No dashes, no leading digit. A table has exactly two keys, fields and
indexes; there is nothing else to configure — per-user and per-workspace scoping is declared in
server code, not here.
#Fields
{ "type": "string", "optional": true, "maxLength": 200 }| Key | Required | |
|---|---|---|
type | yes | One of string, number, boolean, datetime, bytes, json. |
optional | no | true makes the field nullable. Fields are required by default. |
maxLength | no | A positive integer. Valid only on string and bytes. |
Those three keys are all a field has. There is no default, no unique, no foreign key or relation, no
min, and no enum — validate anything else in your handler, where the rule is visible and testable.
Field types map to TypeScript as string, number, boolean, Date, Uint8Array, and a JSON-shaped
value respectively.
#Reserved fields
Every row automatically has:
{ id: string; createdAt: Date; updatedAt: Date }id is minted by the runtime on insert; both timestamps are set for you and updatedAt is maintained.
Declaring any of the three is an error, and writing to them is refused at runtime.
#Indexes
"indexes": { "by_workspace_status": ["workspaceId", "status"] }Each index is an ordered list of at least one declared field. A field that does not exist on the table, or appears twice in one index, is an error.
Indexes are the query surface, not merely an optimisation. A where filter must match { id } or a
declared prefix of a declared index. Given the index above you may filter by { workspaceId } or by
{ workspaceId, status } — and by nothing else. Anything wider is rejected by the generated types and
refused at runtime. See the table API.
There is no cap on the number of tables, fields, or indexes.
#Schema changes
Changing the manifest changes your app's schema. On deploy, a compatible change is applied: a new
table, a new optional field, a widened maxLength, a required field made optional. An incompatible one is
refused, with both schema identities and the exact difference — rather than being applied and hoped for.
Locally, xeer export and xeer import make an incompatible change survivable: export first, change the
manifest, then import into the new schema if it is a compatible successor. There is no forced import.
#storage
Present when — and only when — capabilities includes "storage". Write {} for the platform defaults.
All three fields are optional, and each may be lowered but never raised past its ceiling — a larger
value is a build error, not a silent clamp.
| Field | Default | Maximum |
|---|---|---|
maxObjectBytes | 25 MiB (26214400) | 25 MiB — already the ceiling, so it can only be lowered |
readBytes | 32 MiB (33554432) | 128 MiB (134217728) |
writeBytes | 32 MiB (33554432) | 128 MiB (134217728) |
readBytes and writeBytes are per handler invocation and reset on every call. There is nothing else to
configure: no bucket name, no region, no credential, no tenancy parameter. See
Storage for the ctx.storage surface, the object-key rules, and how
uploads work.
#capabilities
"capabilities": ["database", "storage"]An array of the powers this app is allowed to use. The values are "database" and "storage", and each
must be present exactly when its config block is — declaring one without the other is an error in
either direction, and naming the same capability twice is an error too.
An undeclared capability does not appear on ctx at all, so reaching for it is a compile error
(XE1205) rather than a runtime surprise. See Capabilities.
#budgets
Ceilings the runtime enforces. All optional; each has a default and a maximum.
| Field | Default | Maximum | |
|---|---|---|---|
queryRows | 1000 | 10000 | Rows one query may read. find's limit must fall within it; all() is bounded by it. |
mutationWrites | 100 | 1000 | Writes one mutation may make. |
requestBytes | 1 MiB | 4 MiB | Request body size. Over it: 413 request_too_large. |
responseBytes | 1 MiB | 4 MiB | Response body size. |
liveConnections | 100 | 1000 | Simultaneous live-update streams the app will hold. Past it, a client is told to retry after an interval and honours it. |
Budgets exist so that a runaway query fails loudly and locally rather than becoming a bill or an outage. Raise one when your app genuinely needs it; the value is in the manifest, so the change is reviewable.
#Normalisation
Before your app is built, the manifest is normalised: defaults are filled in, capabilities is sorted, and
$schema is dropped. The normalised form is hashed into your build artifact's identity — which is why two
identical projects produce the identical artifactId, and why promote and
rollback act on an exact version rather than an approximate one.
It is also why the app's identity lives in a separate file,
xeer.project.json: pointing a checkout at
a different app must not change your build hashes.
#Next
- Capabilities — the declare-then-use model.
- Server functions and the database — using what you declared here.
- Diagnostics reference — the
XE10xxandXE11xxfamilies cover this file.