Feature Specification: Schema, Migrations and Module Sources
Created: 2026-09-23
Status: Implemented
Input: Reconstructed from the code of api/ at 0bf4ecd36, api/specification.md, pull requests #1097, #1110, #1127 and #1180, and the design notes of the maintainers.
User Scenarios & Testing (mandatory)
User Story 1 - Get the schema of the configuration (Priority: P1)
A front-end renders a form for a deployment from a JSON Schema. It gets the schema of the default module source, or of a module source that it names. It can ask for the schema of one application, or of one section of the configuration, only.
Why this priority: a front-end cannot make a valid configuration without the schema.
Independent Test: a parity check compares the served schema with the schema that core gives for the same source.
Acceptance Scenarios:
- Given no module source, When the client asks for the schema, Then
apireturns the schema of the default source, without a build at request time. - Given a trusted module source, When the client asks for its schema, Then
apibuilds the schema from the public outputs ofcorein that source, and returns it. - Given an application or a section, When the client asks for its schema, Then
apireturns the part of the schema for it only. An unknown application gets an error that names the known ones.
User Story 2 - Migrate a configuration to a new source (Priority: P1)
The configuration model changes between versions of core. A front-end gets the renames of a source, and api applies them to a stored configuration when a deployment moves to a new source.
Why this priority: an update of the provider must not make the stored configurations invalid.
Independent Test: unit tests rename the options of an old configuration to the options of a new one.
Acceptance Scenarios:
- Given a configuration of an old source, When it moves to a new source, Then
apiapplies the renames of the new source. - Given a migration, When the client does not name the source, Then
apirefuses it. - Given a section, When the client asks for the renames of the section, Then
apireturns only the renames inside the value of that section, with paths relative to that value. - Given a stored value for a key that the new source removed, When the client submits it, Then
apirefuses it with a message that names all keys that the source does not offer, and the remedy.
User Story 3 - Run only trusted module sources (Priority: P1)
A module source is code that api builds and runs. The provider sets the list of trusted repositories. api refuses other sources.
Why this priority: an untrusted fork is equal to remote code execution.
Independent Test: unit tests refuse a source that is not on the list.
Acceptance Scenarios:
- Given a source that is not on the list, When the client uses it to deploy or to get a schema or renames, Then
apirefuses it. - Given an explicit empty list, When the client names any source, Then
apiallows it. - Given no source, When the client submits, Then
apiuses the default source.
User Story 4 - Freeze the default source (Priority: P2)
The default source is a full pin of the revision of the running api. api stores this pin with each submit, so a later update of the provider does not change a deployment without a migration.
Acceptance Scenarios:
- Given a self-pin, When the client submits without a source, Then the build stores this pin.
- Given a build of a dirty tree, When there is no pin, Then the source floats with the running
api.
Edge Cases
- Too many schema builds at the same time:
apirefuses the request at once as unavailable. A build that takes too long:apireports a timeout. - A source that does not give the requested deployment attribute, or that is too old to give a requested output:
apirefuses it with a clear error. - A malformed self-pin counts as no pin.
- The renames of a source cannot load: the migration passes the value through unchanged, and the schema check decides.
- A rename that cannot apply (the shape does not match, or the target already has a value):
apiskips it and names it in the error. It does not guess. - The deploying tree and the pin of a deployment give different versions of a layer: see "The deploying-vs-deployed version boundary (#972)" in
api/specification.md.
Requirements (mandatory)
Functional Requirements
- FR-001:
apiMUST serve the schema, the renames and the catalog of the default source from files made at build time, with no eval at request time. These MUST be public, because they contain no data of a deployment. - FR-002:
apiMUST build the schema, the per-application schemas, the renames and the deployment attributes of another source from the public outputs ofcorein that source. It MUST pass only the provider coordinates that shape the schema, and no deploy credentials. It MUST get the schema of one application fromcore, and MUST NOT cut it from the full schema itself. A missing output or a failed build MUST be an error. - FR-003:
apiMUST limit the number of schema builds at the same time and their duration, MUST refuse a request at once when all slots are in use, and MUST keep recent results in memory for each source. - FR-004: When the trusted list is not empty,
apiMUST run only module sources that exactly match an entry, for deploys and for schema and renames of a given source. By default the list holds only the project repository. An explicit empty list MUST allow all sources. No source MUST always be allowed. The list adds to authorization and does not replace it. - FR-005:
apiMUST store the module source of each build. A migration MUST have an explicit source. - FR-006:
apiMUST serve the schema so that a front-end does not need to know the deployment method or the pin. - FR-007:
apiMUST narrow the schema and the renames to one section when the client asks, so that a front-end does not need to know where the value of the section sits. A section and an application that do not agree MUST be an error. The renames of a section MUST be relative to its value;apiMUST drop the renames outside it, and MUST give an empty set for an unknown section. - FR-008: A rename MUST map a path to a new path, and a segment MAY match any key. Migration MUST be stable in order and idempotent. A rename that cannot apply MUST be skipped and named, not guessed.
- FR-009: The renames MUST come from the alias markers of the module system (what the current pin remembers) and from declared renames of values (a log that is never rewritten). The build MUST check each declared rename against the narrowed schema: its last target MUST be an offered key, and its source MUST be gone unless the chain brings the value back.
- FR-010: A removed key MUST NOT be a rename.
apiMUST refuse a stored value for a key that the source does not offer, MUST list all such keys and the remedy, and MUST NOT drop the value on read. - FR-011: A submit without a source MUST store the self-pin of the running
api. When the default schema cannot be fetched,apiMAY use the built-in schema; for an explicit source it MUST NOT.
Key Entities
- Module source: a pin of a repository (type, URL, revision, hash) that contains
core. - Schema: the JSON Schema (draft 2020-12) of a configuration, from
core. - Renames: the moves of options between versions, from
core. - Trusted list: the repositories that a provider allows as module sources.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: The parity check passes: the served schema is equal to the schema of
core. - SC-002: No request runs a source that is not on a non-empty trusted list.
- SC-003: A slow schema build does not block other requests for longer than the time limit.
Assumptions
- A module source in a submit is a feature for the provider and is not in the forms of a front-end.
- The Nix evaluation cache of the host holds the builds;
apidoes not keep a cache on disk. - The default source is a pin that is baked when the provider deploys
api. The provider deploysapiagain to move the default source forward.
Known Deviations
None.