ObjectStackObjectStack

Rest Server schema — API Protocol reference

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).

Source: packages/spec/src/api/rest-server.zod.ts

TypeScript Usage

import { BatchEndpointsConfigSchema, CrudEndpointsConfigSchema, CrudOperation, EndpointRegistrySchema, GeneratedEndpointSchema, MetadataEndpointsConfigSchema, RestApiConfigSchema, RestServerConfigSchema, RouteGenerationConfigSchema } from '@objectstack/spec/api';
import type { BatchEndpointsConfig, CrudEndpointsConfig, CrudOperation, EndpointRegistry, GeneratedEndpoint, MetadataEndpointsConfig, RestApiConfig, RestServerConfig, RouteGenerationConfig } from '@objectstack/spec/api';

// Validate data
const result = BatchEndpointsConfigSchema.parse(data);

BatchEndpointsConfig

Properties

PropertyTypeRequiredDescription
maxBatchSizeintegeroptional (default: 200)Maximum records per batch operation
enableBatchEndpointbooleanoptional (default: true)Enable POST /data/:object/batch endpoint
operations{ createMany?: boolean; updateMany?: boolean; deleteMany?: boolean }optionalEnable/disable specific batch operations
defaultAtomicneveroptional[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 }.

Nested Shape: BatchEndpointsConfig.operations

PropertyTypeRequiredDescription
createManybooleanoptional (default: true)Enable POST /data/:object/createMany
updateManybooleanoptional (default: true)Enable POST /data/:object/updateMany
deleteManybooleanoptional (default: true)Enable POST /data/:object/deleteMany
upsertManyneveroptional[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.

CrudEndpointsConfig

Properties

PropertyTypeRequiredDescription
operations{ create?: boolean; read?: boolean; update?: boolean; delete?: boolean; … }optionalEnable/disable operations
patternsneveroptional[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.
dataPrefixstringoptional (default: "/data")URL prefix for data endpoints
objectParamStyleneveroptional[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.

Nested Shape: CrudEndpointsConfig.operations

PropertyTypeRequiredDescription
createbooleanoptional (default: true)Enable create operation
readbooleanoptional (default: true)Enable read operation
updatebooleanoptional (default: true)Enable update operation
deletebooleanoptional (default: true)Enable delete operation
listbooleanoptional (default: true)Enable list operation

CrudOperation

Allowed Values

  • create
  • read
  • update
  • delete
  • list

EndpointRegistry

Properties

PropertyTypeRequiredDescription
endpoints{ id: string; method: Enum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>; path: string; object: string; … }[]✅All generated endpoints
totalinteger✅Total number of endpoints
byObjectRecord<string, { id: string; method: Enum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>; path: string; object: string; … }[]>optionalEndpoints grouped by object
byOperationRecord<string, { id: string; method: Enum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>; path: string; object: string; … }[]>optionalEndpoints grouped by operation

Nested Shape: EndpointRegistry.endpoints[number]

PropertyTypeRequiredDescription
idstring✅Unique endpoint identifier
methodEnum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>✅HTTP method
pathstring✅Full URL path
objectstring✅Object name (snake_case)
operationEnum<'create' | 'read' | 'update' | 'delete' | 'list'> | string✅Operation type
handlerstring✅Handler function identifier
metadata{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }optional

Nested Shape: EndpointRegistry.byObject[string][number]

PropertyTypeRequiredDescription
idstring✅Unique endpoint identifier
methodEnum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>✅HTTP method
pathstring✅Full URL path
objectstring✅Object name (snake_case)
operationEnum<'create' | 'read' | 'update' | 'delete' | 'list'> | string✅Operation type
handlerstring✅Handler function identifier
metadata{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }optional

Nested Shape: EndpointRegistry.byOperation[string][number]

PropertyTypeRequiredDescription
idstring✅Unique endpoint identifier
methodEnum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>✅HTTP method
pathstring✅Full URL path
objectstring✅Object name (snake_case)
operationEnum<'create' | 'read' | 'update' | 'delete' | 'list'> | string✅Operation type
handlerstring✅Handler function identifier
metadata{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }optional

GeneratedEndpoint

Properties

PropertyTypeRequiredDescription
idstring✅Unique endpoint identifier
methodEnum<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'>✅HTTP method
pathstring✅Full URL path
objectstring✅Object name (snake_case)
operationEnum<'create' | 'read' | 'update' | 'delete' | 'list'> | string✅Operation type
handlerstring✅Handler function identifier
metadata{ summary?: string; description?: string; tags?: string[]; deprecated?: boolean }optional

MetadataEndpointsConfig

Properties

PropertyTypeRequiredDescription
prefixstringoptional (default: "/meta")URL prefix for metadata endpoints
enableCachebooleanoptional (default: true)Enable HTTP cache headers (ETag, Last-Modified)
cacheTtlneveroptional[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.
maskObjectFieldsbooleanoptional (default: true)[ADR-0106 D8] Mask served object schemas to the caller's readable fields
endpoints{ types?: boolean; items?: boolean; item?: boolean; maintenance?: boolean }optionalEnable/disable specific endpoints

Nested Shape: MetadataEndpointsConfig.endpoints

PropertyTypeRequiredDescription
typesbooleanoptional (default: true)Mount the metadata type list — GET /meta and GET /meta/types (one handler, two paths)
itemsbooleanoptional (default: true)Mount the per-type item list — GET /meta/:type, and nothing else
itembooleanoptional (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
maintenancebooleanoptional (default: true)Mount the whole-store maintenance operations — GET /meta/diagnostics, GET /meta/_drafts and the POST /meta/_migrate-stored write door
schemaneveroptional[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.

RestApiConfig

Properties

PropertyTypeRequiredDescription
versionstringoptional (default: "v1")API version (e.g., v1, v2, 2024-01)
basePathstringoptional (default: "/api")Base URL path for API
apiPathstringoptionalFull API path (defaults to {basePath}/{version})
enableCrudbooleanoptional (default: true)Enable automatic CRUD endpoint generation
enableMetadatabooleanoptional (default: true)Enable metadata API endpoints
enableUibooleanoptional (default: true)Enable UI API endpoints (Views, Menus, Layouts)
enableBatchbooleanoptional (default: true)Enable batch operation endpoints
enableDiscoverybooleanoptional (default: true)Enable API discovery endpoint
enableOpenApibooleanoptional (default: true)Enable OpenAPI 3.1 spec & docs viewer endpoints
enableSearchbooleanoptional (default: true)Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from os serve)
enableProjectScopingbooleanoptional (default: false)Enable project-scoped routing for data/meta/AI APIs
projectResolutionEnum<'required' | 'optional' | 'auto'>optional (default: "auto")Project ID resolution strategy
requireAuthneveroptional[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.
documentation{ title?: string; description?: string; termsOfService?: string; contact?: object; … }optionalPublisher 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
responseFormatneveroptional[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.

Nested Shape: RestApiConfig.documentation

PropertyTypeRequiredDescription
enabledneveroptional[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.
titlestringoptionalTitle of the served OpenAPI document (info.title); unset keeps the bundled title
descriptionstringoptionalDescription of the served OpenAPI document (info.description); unset keeps the bundled description. Your app's own release number belongs here
versionneveroptional[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.
termsOfServicestringoptionalTerms-of-service URL of the served OpenAPI document (info.termsOfService); unset serves none
contact{ name?: string; url?: string; email?: string }optionalContact 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 }optionalLicense of the served OpenAPI document; replaces the bundled info.license whole, so a license without url serves no URL. Unset keeps the bundled license

RestServerConfig

Properties

PropertyTypeRequiredDescription
api{ version?: string; basePath?: string; apiPath?: string; enableCrud?: boolean; … }optionalREST API configuration
crud{ operations?: object; dataPrefix?: string }optionalCRUD endpoints configuration (embedder-only: written by a host that constructs this config, never by os serve or the dev plugin)
metadata{ prefix?: string; enableCache?: boolean; maskObjectFields?: boolean; endpoints?: object }optionalMetadata endpoints configuration (embedder-only: written by a host that constructs this config, never by os serve or the dev plugin)
batch{ maxBatchSize?: integer; enableBatchEndpoint?: boolean; operations?: object }optionalBatch endpoints configuration (embedder-only: written by a host that constructs this config, never by os serve or the dev plugin)
routes{ }optionalRoute generation configuration
openApi31neveroptional[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.

Nested Shape: RestServerConfig.api

PropertyTypeRequiredDescription
versionstringoptional (default: "v1")API version (e.g., v1, v2, 2024-01)
basePathstringoptional (default: "/api")Base URL path for API
apiPathstringoptionalFull API path (defaults to {basePath}/{version})
enableCrudbooleanoptional (default: true)Enable automatic CRUD endpoint generation
enableMetadatabooleanoptional (default: true)Enable metadata API endpoints
enableUibooleanoptional (default: true)Enable UI API endpoints (Views, Menus, Layouts)
enableBatchbooleanoptional (default: true)Enable batch operation endpoints
enableDiscoverybooleanoptional (default: true)Enable API discovery endpoint
enableOpenApibooleanoptional (default: true)Enable OpenAPI 3.1 spec & docs viewer endpoints
enableSearchbooleanoptional (default: true)Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from os serve)
enableProjectScopingbooleanoptional (default: false)Enable project-scoped routing for data/meta/AI APIs
projectResolutionEnum<'required' | 'optional' | 'auto'>optional (default: "auto")Project ID resolution strategy
requireAuthneveroptional[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.
documentation{ title?: string; description?: string; termsOfService?: string; contact?: object; … }optionalPublisher 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
responseFormatneveroptional[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.

Nested Shape: RestServerConfig.crud

PropertyTypeRequiredDescription
operations{ create?: boolean; read?: boolean; update?: boolean; delete?: boolean; … }optionalEnable/disable operations
patternsneveroptional[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.
dataPrefixstringoptional (default: "/data")URL prefix for data endpoints
objectParamStyleneveroptional[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.

Nested Shape: RestServerConfig.metadata

PropertyTypeRequiredDescription
prefixstringoptional (default: "/meta")URL prefix for metadata endpoints
enableCachebooleanoptional (default: true)Enable HTTP cache headers (ETag, Last-Modified)
cacheTtlneveroptional[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.
maskObjectFieldsbooleanoptional (default: true)[ADR-0106 D8] Mask served object schemas to the caller's readable fields
endpoints{ types?: boolean; items?: boolean; item?: boolean; maintenance?: boolean }optionalEnable/disable specific endpoints

Nested Shape: RestServerConfig.batch

PropertyTypeRequiredDescription
maxBatchSizeintegeroptional (default: 200)Maximum records per batch operation
enableBatchEndpointbooleanoptional (default: true)Enable POST /data/:object/batch endpoint
operations{ createMany?: boolean; updateMany?: boolean; deleteMany?: boolean }optionalEnable/disable specific batch operations
defaultAtomicneveroptional[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 }.

RouteGenerationConfig

Properties

PropertyTypeRequiredDescription
includeObjectsneveroptional[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).
excludeObjectsneveroptional[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).
nameTransformneveroptional[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.
overridesneveroptional[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.

On this page