Defines the REST API server configuration for automatically generating RESTful CRUD endpoints, metadata endpoints, and batch operations.
REST API Server Protocol
Defines the REST API server configuration for automatically generating
RESTful CRUD endpoints, metadata endpoints, and batch operations.
Features:
Automatic CRUD endpoint generation from Object definitions
Standard REST conventions (GET, POST, PUT, PATCH, DELETE)
Metadata API endpoints
Batch operation endpoints
OpenAPI/Swagger documentation generation
Architecture alignment:
Salesforce: REST API with Object CRUD
Microsoft Dynamics: Web API with entity operations
Strapi: Auto-generated REST endpoints
WHO CAN WRITE THIS CONFIG (#15543) — read this before planning a deployment
around any key below. A RestServerConfig is the ARGUMENT a host passes when
it constructs the server: never a stack collection member and never a stored
metadata row.
There is exactly ONE door, and it is programmatic:
createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts),
whose start() is the only non-test site that reaches new RestServer(...).
⚠️ HonoServerPlugin (packages/plugins/plugin-hono-server) declares a
restConfig?: RestServerConfig option, but it is NOT a second door: its one
reader takes api.basePath for the SPA fallback, and that plugin never
constructs a REST server, so nothing below api.basePath reaches anything.
No shipped boot path opens that door with a config of its own. os serve
(packages/cli/src/commands/serve.ts) reads the stack config's own top-level
api: block and forwards exactly two keys out of it —
api.enableProjectScoping and api.projectResolution — into a fixed
argument, through an as any cast; the dev plugin
(packages/plugins/plugin-dev/src/dev-plugin.ts) calls
createRestApiPlugin() with no config at all. So the CLI does read a config
file — it just forwards those two keys and nothing else.
⇒ On a CLI-started deployment every OTHER key here is EMBEDDER-ONLY: the
whole of crud, metadata and batch, and the rest of api. Its value is
whatever the .default() below says, and no flag, config file or CLI option
moves it.
⚠️ ONE CARVE-OUT, and it is the security-relevant key: the REST server's
normalizeConfig folds the environment into the effective value of
metadata.maskObjectFields — OS_ALLOW_UNMASKED_OBJECT_METADATA turns the
ADR-0106 D8 mask OFF for a deployment that cannot otherwise reach the key.
That env var is the only thing outside an embedder's argument that changes
any value here. See maskObjectFields below.
This is the recorded posture, not a gap awaiting a fix: the keys keep their
runtime reads and their embedder consumer, and the reachability answer
ADR-0049 asks for is written per key in the liveness ledger
(packages/spec/liveness/crud_endpoints.json, metadata_endpoints.json,
batch_endpoints.json).
[REMOVED] batch.defaultAtomic was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no batch handler ever consulted it: atomicity is decided per request by options.atomic in the batch body (BatchOptions, ADR-0119 D4, opt-in), and a server-side default that flipped it silently would change the failure semantics of callers who send nothing. Delete the key; a caller that needs all-or-nothing sends options: { atomic: true }.
[REMOVED] batch.operations.upsertMany was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it gated a route that was never built: there is no POST /data/:object/upsertMany and no protocol member behind it, so false disabled nothing. Delete the key. Upsert is an operation type of the generic POST /data/:object/batch endpoint (BatchOperationType'upsert', keyed by externalId), gated by batch.enableBatchEndpoint.
[REMOVED] crud.patterns was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: every CRUD route is mounted from the fixed method/path pairs in the REST server's registerCrudEndpoints, so a custom pattern was validated and ignored. Delete the key. The mounted CRUD paths are the contract the client SDK, the discovery document and the served /openapi.json all describe, and crud.dataPrefix is the one live knob that moves them; an endpoint on a custom path or method is a declarative api endpoint (type: 'object_operation', ApiEndpoint in @objectstack/spec/api), which is matched, executed and documented.
dataPrefix
string
optional (default: "/data")
URL prefix for data endpoints
objectParamStyle
never
optional
[REMOVED] crud.objectParamStyle was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: every CRUD route takes the object name as a path segment, so 'query' was validated and mounted exactly what 'path' mounts. Delete the key; the object name is always a path segment.
[REMOVED] metadata.cacheTtl was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: metadata.enableCache selects the protocol's getMetaItemCached read path, which takes no TTL, and no Cache-Control / ETag header was ever built from this value. Delete the key; metadata.enableCache is the live switch, and a declarative api endpoint's cacheTtl is the key that does reach the wire.
maskObjectFields
boolean
optional (default: true)
[ADR-0106 D8] Mask served object schemas to the caller's readable fields
Mount the metadata type list — GET /meta and GET /meta/types (one handler, two paths)
items
boolean
optional (default: true)
Mount the per-type item list — GET /meta/:type, and nothing else
item
boolean
optional (default: true)
Mount the whole per-item face — GET, PUT and DELETE /meta/:type/:name, its /references and /layers reads, the history family (/history, /audit, /diff, /published, /publish, /rollback) and GET /meta/book/:name/tree
maintenance
boolean
optional (default: true)
Mount the whole-store maintenance operations — GET /meta/diagnostics, GET /meta/_drafts and the POST /meta/_migrate-stored write door
schema
never
optional
[REMOVED] metadata.endpoints.schema was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — it gated a route that does not exist: the REST server mounts no GET /meta/:type/:name/schema, so false removed nothing and true added nothing. Delete the key; endpoints.types / items / item / maintenance are the switches that gate real mounts.
Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from os serve)
enableProjectScoping
boolean
optional (default: false)
Enable project-scoped routing for data/meta/AI APIs
projectResolution
Enum<'required' | 'optional' | 'auto'>
optional (default: "auto")
Project ID resolution strategy
requireAuth
never
optional
[REMOVED] api.requireAuth was removed in @objectstack/spec 17. Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (sharing.allowAnonymous), a share link, or book.audience: 'public' — each derives its own narrow authorization instead of opening the whole data plane. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
Publisher identity of the served OpenAPI document: each member set here overlays its info on both /openapi.json doors, and nothing set serves the bundled info unchanged. info.version is always the protocol version
responseFormat
never
optional
[REMOVED] api.responseFormat was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: envelope, includeMetadata and includePagination were parsed, defaulted and copied into the REST server's config and never consulted, so envelope: false unwrapped no response. Delete the key. Response shapes are fixed, not a server-wide option: each route answers in the response schema @objectstack/spec/api declares for it, which is what the client SDK parses and the served /openapi.json describes, so no configuration changes them.
[REMOVED] api.documentation.enabled was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling api.enableOpenApi at the mount, so enabled: false turned nothing off. Delete the key; api.enableOpenApi: false is the switch that leaves the /openapi.json document and its /docs viewer unmounted.
title
string
optional
Title of the served OpenAPI document (info.title); unset keeps the bundled title
description
string
optional
Description of the served OpenAPI document (info.description); unset keeps the bundled description. Your app's own release number belongs here
version
never
optional
[REMOVED] api.documentation.version was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it, and the served OpenAPI document's info.version has one source: the protocol version, i.e. the version of the @objectstack/spec package that generated the document, which no deployment configuration overrides. Delete the key. To publish your app's own release number, write it into api.documentation.description, which the served info.description carries.
termsOfService
string
optional
Terms-of-service URL of the served OpenAPI document (info.termsOfService); unset serves none
contact
{ name?: string; url?: string; email?: string }
optional
Contact of the served OpenAPI document; replaces the bundled info.contact whole, so a member left out is absent rather than inherited. Unset keeps the bundled contact
license
{ name: string; url?: string }
optional
License of the served OpenAPI document; replaces the bundled info.license whole, so a license without url serves no URL. Unset keeps the bundled license
Batch endpoints configuration (embedder-only: written by a host that constructs this config, never by os serve or the dev plugin)
routes
{ }
optional
Route generation configuration
openApi31
never
optional
[REMOVED] RestServerConfig.openApi31 was removed in @objectstack/spec 17 (ADR-0049) — no runtime ever read it: the REST server forwards only api/crud/metadata/batch/routes, and the served /openapi.json is the pre-generated contract enriched with the live server URL and the registered objects, so webhook/callback definitions declared here never appeared in it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring the key; for a real outbound webhook use Webhook from @objectstack/spec/automation.
Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from os serve)
enableProjectScoping
boolean
optional (default: false)
Enable project-scoped routing for data/meta/AI APIs
projectResolution
Enum<'required' | 'optional' | 'auto'>
optional (default: "auto")
Project ID resolution strategy
requireAuth
never
optional
[REMOVED] api.requireAuth was removed in @objectstack/spec 17. Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (sharing.allowAnonymous), a share link, or book.audience: 'public' — each derives its own narrow authorization instead of opening the whole data plane. Run os migrate meta --from 16 to list the mechanical edits for existing sources; --write applies the ones it can prove, and you apply the rest by hand.
Publisher identity of the served OpenAPI document: each member set here overlays its info on both /openapi.json doors, and nothing set serves the bundled info unchanged. info.version is always the protocol version
responseFormat
never
optional
[REMOVED] api.responseFormat was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: envelope, includeMetadata and includePagination were parsed, defaulted and copied into the REST server's config and never consulted, so envelope: false unwrapped no response. Delete the key. Response shapes are fixed, not a server-wide option: each route answers in the response schema @objectstack/spec/api declares for it, which is what the client SDK parses and the served /openapi.json describes, so no configuration changes them.
[REMOVED] crud.patterns was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: every CRUD route is mounted from the fixed method/path pairs in the REST server's registerCrudEndpoints, so a custom pattern was validated and ignored. Delete the key. The mounted CRUD paths are the contract the client SDK, the discovery document and the served /openapi.json all describe, and crud.dataPrefix is the one live knob that moves them; an endpoint on a custom path or method is a declarative api endpoint (type: 'object_operation', ApiEndpoint in @objectstack/spec/api), which is matched, executed and documented.
dataPrefix
string
optional (default: "/data")
URL prefix for data endpoints
objectParamStyle
never
optional
[REMOVED] crud.objectParamStyle was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: every CRUD route takes the object name as a path segment, so 'query' was validated and mounted exactly what 'path' mounts. Delete the key; the object name is always a path segment.
[REMOVED] metadata.cacheTtl was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: metadata.enableCache selects the protocol's getMetaItemCached read path, which takes no TTL, and no Cache-Control / ETag header was ever built from this value. Delete the key; metadata.enableCache is the live switch, and a declarative api endpoint's cacheTtl is the key that does reach the wire.
maskObjectFields
boolean
optional (default: true)
[ADR-0106 D8] Mask served object schemas to the caller's readable fields
[REMOVED] batch.defaultAtomic was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no batch handler ever consulted it: atomicity is decided per request by options.atomic in the batch body (BatchOptions, ADR-0119 D4, opt-in), and a server-side default that flipped it silently would change the failure semantics of callers who send nothing. Delete the key; a caller that needs all-or-nothing sends options: { atomic: true }.
[REMOVED] routes.includeObjects was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: route registration iterates every registered object, so an include list still mounted all of them. Delete the key. Per-object API exposure is declared on the object and enforced by the REST data surface: enable.apiEnabled: false hides the object (404) and enable.apiMethods whitelists its operations (405 for the rest).
excludeObjects
never
optional
[REMOVED] routes.excludeObjects was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: route registration iterates every registered object, so an excluded object was still mounted. Delete the key. Per-object API exposure is declared on the object and enforced by the REST data surface: enable.apiEnabled: false hides the object (404) and enable.apiMethods whitelists its operations (405 for the rest).
nameTransform
never
optional
[REMOVED] routes.nameTransform was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read it: the enum was validated and every value mounted exactly what 'none' mounts. Delete the key; the object name is the canonical id on every surface, the REST path segment included, so there is no URL transform to configure.
overrides
never
optional
[REMOVED] routes.overrides was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the per-object record: enabled, basePath and operations were validated and turned nothing on or off. Delete the key. Per-object exposure lives on the object (enable.apiEnabled hides it, enable.apiMethods whitelists its operations — both enforced by the REST data surface); the data base path is deployment-wide (crud.dataPrefix); and an endpoint on a custom path is a declarative api endpoint.