Skip to main content

The API surface document

apps/api/openapi.json records the declared core API routes and statically collected built-in integration routes. It is generated and committed so route, method, authentication and adopted wire-contract changes appear in review. The generator needs no running server, database, cache or network connection. It describes the built-in surface, not which integrations are enabled on a particular deployment. Development-only host endpoints and Better Auth's individual wildcard subroutes are not expanded into separate operations.

When changing a route:

  1. Register core routes through registerCoreRoutes, or use literal ctx.registerRoute declarations for built-ins.
  2. Keep the auth declaration beside the actual runtime guard. Metadata alone never enforces authentication.
  3. For an adopted SDK family, update its canonical request/response contract and verify client-visible serialization.
  4. Generate OpenAPI, then regenerate the SDK if its input contract changed.
  5. Inspect the diffs and run the focused route/transport tests and freshness checks.
pnpm openapi:generate
pnpm check-openapi
# When an adopted generated-client contract changes:
pnpm sdk:generate
pnpm check-api-sdk

check-openapi runs in CI. The current pre-commit hook checks the staged snapshot's formatting and lint; it does not run OpenAPI generation or the full policy suite. apps/api/scripts/openapi/__tests__/openapi-freshness.test.ts checks the committed document, authentication classification and admin-route classification.

What gets collected, and how​

The document has two halves, gathered two different ways.

Core routes​

registerCoreRoutes (apps/api/src/routes/index.ts) is the single source of truth for every route not contributed by an integration. The generator mounts it on a bare Fastify instance, waits for ready(), and reads the route table back through @fastify/swagger. Nothing listens on a port; the database and cache clients are lazy and never connect.

If you add a core route, register it through registerCoreRoutes. A route declared directly on the server instance in server.ts would be served but absent from the document. This is why /health, /api/id-schemes and /api/transit/registry live in routes/meta.ts rather than inline.

Whatever Fastify JSON schema a route declares (querystring, params, body, response) flows through into the document. Inventory-only operations can describe only the path, method and auth level. Adopted contract families also define payloads, statuses and headers. Check the operation itself before treating it as a complete wire contract.

Integration routes​

Integration routes are not Fastify routes. ctx.registerRoute(...) appends to a private table (apps/api/src/integration-routes.ts) that is dispatched from two wildcard routes, so Fastify's own introspection sees /api/integrations/:id and nothing else.

They are therefore read statically, by parsing the registerRoute call sites with the TypeScript compiler API across the built-in integration tree. Running every integration's setup() against a stub context executes arbitrary integration code, including registry loads, inside a gate. Static collection avoids those side effects. The static collector describes declarations without proving that an enabled integration reaches each registration at runtime. Focused setup and route tests verify that behavior.

The consequence is a constraint: registerRoute must be called with literal method and path strings. A computed path fails the scan with an explanatory error rather than silently vanishing from the document.

Shared route factories​

A helper in packages/integration-framework can register routes on behalf of the integrations that call it: createTidesIntegration does, contributing GET /tides to four integrations. Those are declared in SHARED_ROUTE_FACTORIES in collect-integration-routes.ts.

If you add another such helper, add it to that list. You will not forget silently: the scanner fails when a framework module registers routes without being declared.

Declaring how a route authenticates​

Every operation carries x-openmapx-auth. For integration routes it is read from registerRoute's requireAuth option. Omitted options or a literal options object without requireAuth mean public; an explicit requireAuth must be a literal true or false. The scanner rejects options identifiers, spreads, computed property names, prototype assignments, duplicate auth declarations and nonliteral auth values with the source file and route in its diagnostic. Keep these declarations literal rather than executing integration code during generation. Core routes need authentication stated, because guards run inside handlers or in a plugin preHandler hook and nothing about them is introspectable.

declareRouteAuth records metadata only. State the classification once per plugin, next to the guard it describes:

export const savedRoute: FastifyPluginAsync = async (fastify) => {
declareRouteAuth(fastify, "session");

// Every /saved route is per-user; authenticate once here so no handler can
// forget the check.
fastify.addHook("preHandler", requireAuthHook);

fastify.get("/saved/lists", async (req) => {
/* ... */
});
};

A single route can override the plugin-wide level:

fastify.post("/mobile-auth/issue", { config: { auth: "session" } }, handler);
LevelMeaning
publicNo credentials required.
sessionA signed-in user session.
adminAn administrator session.
privacy_adminA least-privilege privacy caseworker or full administrator session.
serviceAn administrator session or a service-to-service token (/api/data-manager).
internalNo application check: reachability is expected to be restricted by the network (/api/internal/*).
unspecifiedNobody has classified it. Fails the freshness test.

unspecified is deliberately distinct from public: an unclassified route must never be documented as open.

Status and mobile-auth request boundaries​

GET /api/status is the cookie-independent, redacted status representation. It may be shared-cacheable and never includes dependency URLs, probe errors, or refresh error classes. GET /api/admin/status requires an administrator session and returns operational detail with Cache-Control: private, no-store. Both representations report snapshotAgeMs and stale; stale data is bounded to five minutes.

The two mobile session-handoff operations accept JSON bodies of at most 4096 bytes. Fastify enforces that parser limit for fixed-length and chunked requests before either handler runs. Their schemas reject unknown keys and use these field bounds:

OperationFieldAccepted value or length
POST /api/mobile-auth/issuepurposesign-in, link-provider, or add-passkey
codeChallenge43 to 128 base64url characters
state16 to 128 base64url characters
POST /api/mobile-auth/exchangecallbackCode16 to 256 base64url characters
codeVerifier43 to 128 base64url characters
state16 to 128 base64url characters

Every application POST, PUT, PATCH, or DELETE request that carries a Cookie header must also carry exactly one syntactically valid HTTP(S) Origin. Its normalized scheme, hostname, and effective port must exactly match an origin configured in CORS_ORIGIN. Wildcards, sibling or suffix matches, credentials, paths, queries, fragments, custom schemes, null, and a missing or repeated Origin are rejected with a generic 403 before body parsing or route handling. There is deliberately no application-level Referer fallback.

The only path exemption is exactly /api/auth and its /api/auth/ subtree, whose unsafe requests remain protected by Better Auth's own global origin middleware. Cookie-free bearer, service-token, loopback-CLI, and anonymous requests continue to their existing route authentication; that is an authentication-mode branch, not a route exemption. In particular, loopback admin mutations still require X-OpenMapX-Local-Admin independently of this guard. GET, HEAD, and OPTIONS are unaffected.

CORS_ORIGIN is also the normalized web-origin source for CORS, Better Auth, and auth UI redirects. CORS responses vary on Origin and never reflect an untrusted origin. CORS is a browser response policy, not authorization, and it does not replace the mutation guard.

What this document is not​

The inventory is an internal application surface, not a promise of a stable versioned public API. Better Auth is mounted as one wildcard handler and appears as /api/auth/{wildcard}; its individual endpoints remain Better Auth's contract.

Adding schemas changes runtime behavior as well as documentation:

  • A Fastify response schema controls serialization through fast-json-stringify and can silently drop undeclared fields. Verify the actual response and client before adopting one.
  • querystring and body schemas validate before the handler and can change HTTP 400 behavior. Adopt them per route with tests.
  • Generated clients cover only explicitly adopted families. An inventory entry alone provides no payload validator or SDK method.

When the gate fails​

✗ apps/api/openapi.json is out of date — the API surface changed.
Run `pnpm openapi:generate` and commit the result alongside your route change.

Regenerate and read the diff before committing it. Investigate unexpected routes, missing fields or weaker auth declarations before accepting the diff. Do not refresh the document merely to hide an unexplained change.

Generated runtime metadata SDK​

The application SDK generator selects each adopted family separately:

FamilyCanonical source under packages/core/src/Consumer entry point
Runtime metadataapi/contracts/runtime-metadata.ts@openmapx/core/api-sdk
Data-manager administrationapi/contracts/data-manager-admin/@openmapx/core/api-admin-sdk
Activity administrationapi/contracts/activity-admin.ts@openmapx/core/api-admin-sdk
Integration administrationapi/contracts/integration-admin/@openmapx/core/api-admin-sdk
Service/workflow administrationapi/contracts/service-admin/@openmapx/core/api-admin-sdk
Extension administrationapi/contracts/extension-admin/@openmapx/core/api-admin-sdk
Internal data-managerservices/data-manager/contracts/@openmapx/core/data-manager-sdk

The internal data-manager client has separate generation commands and service credentials. The browser administration client uses application API authentication. Do not substitute one transport for the other.

GET /api/capabilities and GET /api/integrations have shared Zod response contracts in @openmapx/core/api-contracts. Their real Fastify response schemas feed named OpenAPI components. Other inventory-only endpoints are adopted in separate families when their actual wire contracts have been specified.

After changing these contracts, run pnpm openapi:generate, then pnpm sdk:generate. Commit both the OpenAPI diff and generated SDK diff. pnpm check-api-sdk checks the complete generated file set without rewriting it and runs in the policy gate. Generation uses the committed OpenAPI document, selects the runtime-metadata family before validation, and performs no provider or database acquisition. Unrelated inventory entries are not SDK input.

Import operations from @openmapx/core/api-sdk. Pass an isolated ApiClient through the operation's options for server/native or independently configured browser acquisition. The generated integration operation also accepts typed conditional request headers as its first argument. Its result distinguishes 200 with metadata from bodyless 304. The transport owns credentials, cancellation through body consumption, deadlines and bounded redacted HTTP errors; the generated validator owns the response shape. Contract errors do not expose validation inputs. Legal pages use one cache: "no-store" snapshot.

The pinned Orval configuration enables reusable recursive Zod schemas and selects generated output types for validated custom-client responses using its supported output transformer. Both are required to preserve recursive catalog types. Do not remove them without running SDK type/behavior tests. Public integration health metadata contains type, optional name and optional category; operational URLs, headers and configuration keys stay inside the host. Source licensing uses the portable structured output schema.

Generic weather forecasts​

GET /api/integrations/weather/forecast returns independent hourly and daily WeatherForecastResponse envelopes. Each envelope contains points, the exact selected source, and captured contributors projected from its returned points. Hourly and daily fallback can select different suppliers. A successful empty forecast keeps its selected source with empty points and receipts; when no provider succeeds, the source is null and both arrays are empty.

Each forecast point retains its own canonical contributors, including authoritative [], so clients can project credits after filtering or selecting points. The orchestrator captures the lookup catalogue before each provider acquisition and resolves only the response's explicit source or supplied receipts. Cached responses keep that acquisition metadata; they never rebuild credits from the current integration catalogue. The versioned forecast cache namespace excludes older bare-array payloads.

The cache identity also includes the current disallowed-source set so a policy change reacquires an eligible fallback. If an adapter identifies its selected feed only in the response, the orchestrator rejects a disallowed returned source before exposing it. Such adapters still enforce acquisition policy internally. The host response filter remains the final authority: a policy change after handler resolution can replace a source-tagged envelope with null, as for other source-tagged API results.

Canonical air-quality routes​

The built-in air-quality integration owns three public, expensive-tier routes. They return Cache-Control: private, max-age=0; provider and distributed caches reduce upstream work without turning location-specific responses into shared browser-cache entries.

RouteRequired queryOptional queryBounds
GET /api/integrations/air-quality/currentlat, lngcountryCode, subdivisionCode, comparisonStandardWGS84 coordinates; uppercase ISO hints
GET /api/integrations/air-quality/forecastlat, lngcurrent options plus hourshours defaults to 48 and is 1 to 120
GET /api/integrations/air-quality/stationssouth, west, north, eastzoom, pollutant, limit, cursorviewport at most 20° latitude × 30° longitude; antimeridian supported; zoom 0 to 22; limit 1 to 500

Every scalar key may occur once. Invalid/repeated values return HTTP 400 with { code: "INVALID_QUERY", message, details? }. A valid request with no evidence is HTTP 200 with status: "unavailable", never 204. partial means useful evidence was returned while a provider, comparison, freshness, policy, or size condition degraded the result.

Current responses name primaryEvidenceId independently from primaryIndexId. Raw fallback deliberately sets the evidence ID and leaves the index ID null. A requested comparison standard is secondary and cannot replace the standard resolved from the location. Forecasts return a bounded evidence table, stable provider series, and validity-start frames; categories are never interpolated. Stations are GeoJSON Points, thinned deterministically to one winner per zoom-derived Web Mercator cell.

Station cursors are signed, purpose- and query-bound references into an immutable five-minute snapshot. They do not contain station data. Tampered or cross-query cursors are HTTP 400; an expired snapshot/cursor or changed source policy is HTTP 409 CURSOR_EXPIRED. Non-2xx canonical responses use only the closed codes INVALID_QUERY, DOMAIN_DISABLED, CURSOR_EXPIRED, FRAME_UNAVAILABLE, UPSTREAM_INVALID_RESPONSE, and NORMALIZED_RESPONSE_TOO_LARGE.

The deprecated GET /api/integrations/weather-open-meteo-air-quality/aqi route preserves its nine-field body. It now advertises Deprecation, Sunset, and a canonical successor Link. Source-policy exclusion is intentionally HTTP 503 with exactly { "message": "Open-Meteo air quality is disabled by data-use policy" }.

Internal data-manager service​

services/data-manager/openapi.json describes all explicit internal operations, including progress streams, binary downloads and HEAD responses. It is collected from the actual registerApi route registrations without starting jobs or connecting to a database. Optional offline-package routes are included using an inert collaborator. Automatic HEAD routes are excluded; explicit HEAD operations remain part of the contract.

The canonical Zod operation table lives in packages/core/src/services/data-manager/contracts. It owns method, path, authentication, request validation, response statuses, wire schemas and required headers. Register producer handlers through registerDataManagerRoute so runtime validation and documentation use that same definition. Validate each hijacked NDJSON event before writing it; these streams bypass Fastify serialization.

pnpm data-manager:openapi:generate
pnpm data-manager:sdk:generate
pnpm check-data-manager-openapi
pnpm check-data-manager-sdk

The last two commands run in check:policy. Commit both the document and generated SDK when changing a contract. Orval generates request serialization and plain wire types; operation-bound transport closures select the canonical runtime validators. Do not edit generated files or introduce a separate handwritten endpoint map.

Server consumers import @openmapx/core/data-manager-sdk, explicitly construct a transport with their service URL and credentials, and call its generated functions. The transport enforces destination policy, deadlines, response sizes, declared statuses, media types, headers and payloads. Workflow adapters consume bounded, validated NDJSON progress and require a terminal event. Binary operations return an unconsumed Response; callers own streaming and release. This internal SDK must never receive browser credentials or replace the browser-facing API client.

Browser-facing data-manager administration​

The application API has a separate data-manager-admin contract family covering all 19 /api/data-manager operations and six /api/admin/ambient-places operations. Its portable schemas live under packages/core/src/api/contracts/data-manager-admin; API-owned database, Redis, operations-agent and merged-catalog projections are defined there rather than pretending they are internal data-manager responses.

Register these handlers through registerDataManagerAdminRoute and keep their actual authentication guards beside the corresponding declarations. Authentication metadata does not grant or enforce access. The collector checks all 25 actual registrations, their stable operation IDs, status schemas and authentication classifications. Shared schema emission supports Fastify's draft-7 runtime and the OpenAPI 3.1 document without maintaining parallel handwritten schemas.

pnpm openapi:generate
pnpm sdk:generate
pnpm check-openapi
pnpm check-api-sdk

Browser callers import @openmapx/core/api-admin-sdk. Generated named operations use ApiClient for the configured API origin, cookies, cancellation and deadlines. Their bound transport validates each declared status and its canonical JSON schema within response-size limits. unwrapAdminResponse returns successful data or a structured, redacted ApiClientError. Handle an expected absence such as an index status 404 before unwrapping; it is distinct from a malformed response or a successful published index. Query keys, polling and invalidation remain owned by the browser feature.

The metadata and administration clients are generated together, with independent schema selection and one exact artifact-set check. Mechanical file emission and operation binding are shared with the internal SDK. Neither browser client uses internal service credentials or data-manager destinations.

Activity administration​

The activity-admin family adopts exactly five core operations: list jobs, inspect one source-qualified job, cancel an application job, read stream metrics and open an application-job SSE connection. activityAdminOperations in @openmapx/core/api-contracts supplies their request/response schemas. Routes use the shared administration registrar after their inherited admin guard; the older data-manager family retains its existing validation phase and hooks.

The list query defaults to limit=50 and offset=0; optional pagination accepts only bounded safe decimal integers. Detail and cancellation distinguish application and data-manager sources. Complete wire jobs include ISO dates, recursive JSON payload/result values, actors in list rows, logs and data-manager stages. Invalid output is rejected before Fastify can silently drop fields.

Import the generated operations from @openmapx/core/api-admin-sdk. Finite operations return status-qualified validated responses. streamAdminActivityJobEvents returns an AdminEventStreamTarget with url and withCredentials; construct native EventSource in the browser and retain ownership of its lifetime. It does not fetch or parse SSE as JSON. The optional query cursor resumes an initial connection; native reconnections use Last-Event-ID, which takes precedence. Validate each received frame with adminJobStreamEventSchemas before applying it.

The raw producer validates snapshots and every event before writing. Both the snapshot priming backlog and unflushed writes are bounded to 200 per viewer; overflow emits a qualified control event and closes. Invalid post-hijack state closes without exposing producer content, unsubscribes and clears heartbeat timers. Regenerate both pnpm openapi:generate and pnpm sdk:generate after a contract change, then run pnpm check-openapi and pnpm check-api-sdk.

Integration administration​

integrationAdminOperations owns all 22 integration, binding, credential and /api/admin/overview operations. Import the generated functions and canonical wire types from @openmapx/core/api-admin-sdk. The focused producer plugins inherit the administrator guard before contract parsing. Binding POST retains its earlier body-validation-before-handler-auth ordering. JSON parsing, global CSRF and rate limits retain their existing lifecycle positions. An omitted optional mutation body stays absent; explicit JSON null is invalid.

Manifest/config-schema JSON remains lossless. Credential status uses the actual resolved winning layer, with stored-vault audit fields reported separately and without values. Health-history hours default to 24 and accept one integer from 1 through 168. SQL timestamp-without-time-zone hour values are interpreted as UTC and emitted as ISO timestamps. Reload enqueue responses are HTTP 200 {ok, jobId}; they do not establish terminal job success.

Configuration import validates the complete file against current manifests before writes. It replaces named non-secret overrides, preserves an omitted enabled, and leaves unnamed rows and the vault untouched. Declared top-level secret fields are omitted. Unknown/read-only/invalid fields and masked non-secret values reject the file. Exports omit secrets, obsolete/unknown DB fields and read-only fields; their optional exportedAt and count metadata is accepted by import. Empty imports are rejected, including an empty export with no saved overrides.

Validation errors identify bounded entry numbers, known integration/field names and public rule categories. They never include supplied values, unknown keys, raw schema values or internal validator details.

One DB transaction persists the batch. Before preparing the complete input, the API observes the current host revision through typed configuration.inspect; the initial stack.render must still own that revision at activation. A completed competing writer causes a conflict, with no automatic mutation retry. One normal reload follows a successful apply. A failed import returns HTTP 409 with ok: false, phase, recovery, database, optional runtime, and optional apply/compensation outcome evidence. recovery: "restored" is still a failed import, not success. reconciliation_required means an operator must inspect saved, applied and runtime state before another deliberate action; do not retry a mutation automatically. No secret-bearing snapshot enters this response.

Recovery compares each named row's identity and PostgreSQL tuple version in an all-or-none transaction. It reverses only those integrations in the full payload actually submitted, preserving unrelated changes included during preparation. Before using previous DB rows as an inverse, recovery compares their named projections against fingerprints from that same authoritative host observation. Missing evidence or prior saved/applied divergence returns reason: "previous_applied_not_proven", retains the imported DB rows, and requires reconciliation without replaying unproven values. The observation exposes no configuration values or secrets. An initial host conflict also requires reconciliation even if owned DB rows were restored: the competing generation may have included some of the imported settings.

Host compensation authenticates expectedCurrentRevisionId using the committed import revision. A newer host generation conflicts rather than being overwritten. After a reload exception, a successful DB/host restoration also needs a recovery reload and a final DB ownership check. Unknown completion, lost ownership, compensation failure and incomplete cleanup remain explicit reconciliation. This is request-local compensation, not a crash-atomic DB/host/runtime transaction; process loss can require operator reconciliation. Live PostgreSQL and deployment acceptance are separate from provider-independent fixture qualification.

Ordinary browser mutation completion effects capture the executing client and integration/credential owner. Replacement origins, identities and unmounted views cannot receive old callbacks, input clearing or feedback. Successful work can refresh its originating caches. Disposing the view does not cancel accepted host work; reload acceptance remains separate from Activity terminal completion.

Service and workflow administration​

serviceAdminOperations owns service inventory/detail, configuration, credentials, actions, logs, selection, backups, deployment/Compose, operation catalogue, shared-mobility rollback and Dawarich provisioning/rotation contracts. Producers use registerServiceAdminRoute after the runtime administrator guard. Import generated operations and canonical wire types from @openmapx/core/api-admin-sdk.

Some statuses carry operational meaning that callers must preserve. Compose down returns an intentional 503 failure instead of a successful shutdown contract; managed platform credentials can return 409; queued actions return job acceptance without proving terminal completion. Handle expected status variants before unwrapping a response. Do not convert an unknown host outcome into a successful retry or rollback.

streamAdminServiceLogs uses a bounded text-stream transport rather than JSON validation. The caller owns cancellation, consumption and release. The separate Activity observer owns accepted-job terminal effects; log closure alone cannot prove a service action completed. The generated operation table, transport and focused producer tests are the authority for each status and lifetime.

Regenerate OpenAPI before the SDK and run both freshness checks after changing this family. Host configuration revision ownership is documented in Service manifest.

Extension administration​

extensionAdminOperations owns all eleven catalog, installed-extension, preview, install/update/remove, source and refresh operations. Producers use the shared administration registrar after the inherited admin guard. The independent extension preview limiter and the shared install/update store limiter run before structural validation; global CSRF, public rate limits and JSON parsing retain their lifecycle positions.

Import the generated functions and canonical wire types from @openmapx/core/api-admin-sdk. Catalog list and detail are distinct projections; unfamiliar nested JSON remains inert and lossless, and unknown versions remain absent. Installed rows expose ISO timestamps and optional registry enrichment. Installed hasUpdate is boolean | null: true means a different matching catalog version, false means the same version, and null means comparison is unavailable (catalog failure, absent entry or missing version). latestVersion is null when unavailable; installedVersion remains authoritative local data. Catalog-list hasUpdate remains boolean. Optional catalog failures do not hide local inventory; database and response-validation failures still fail the read. The browser retains current-origin cached inventory after a failed refetch with a stale warning and removal controls, but disables update using stale evidence. Preview responses retain all effective ports, the inspected repository commit, full security rating and confirmation fingerprint. Install, update and remove return HTTP 202 {jobId} acceptance; Activity observation owns terminal outcomes. Mutations are never retried automatically.

Requests reject unknown keys, wrong types and explicit JSON null. Update may omit its body; remove and refresh accept absence or an empty object. Selectors retain ID precedence when both are supplied. Source strings retain exact identity; URL, trust, provenance, revocation, persistence and host authorization remain domain responsibilities. Regenerate OpenAPI then SDK and run both freshness checks after contract changes.

Direct manifest URLs use the real safe-fetch path and full portable manifest validation, then apply revocation and platform policy to the validated identity. They always resolve at community trust. Catalog resolution retains ID, declared version, digest and immutable verified authorization checks. All critical entries block preview/install/update and worker execution until cleared; maxVersion remains display-only metadata. Removal stays available. Existing stale-feed reporting and cached denial semantics are preserved.

Preview has its own one-token/30-second network-key bucket. Install and update retain their shared bucket at the same limit; other store callers are unchanged. The guard still runs before the limiter and canonical structural validation. The HTTP preview and install/update endpoints have independent buckets, so an authenticated UI can submit a reviewed fingerprint directly to install/update. The operator CLI obtains another preview and therefore must also respect the preview bucket. Neither path automatically retries mutations.

Extension-store validation errors select fixed public categories. Unknown provider, JSON, staging and source-persistence failures use fixed operation messages; route diagnostics contain only the operation message and safe error class. Never return or log arbitrary dependency messages, URLs, schema values or filesystem paths. Detail resolution failure retains its 200/empty-component fallback with safe diagnostics. The canonical wire schemas and generated SDK signatures are unchanged by these domain corrections.

The browser extension consumer uses the generated family and canonical inferred contracts, including separate catalog list/detail projections and inert unknown JSON metadata. Its keys retain the raw configured API identity, including an empty same-origin value or trailing slash; only transport URL construction resolves the empty value against the document origin. Configured API bases must be HTTP(S) origins, without path prefixes, query strings or fragments. No global client is reconfigured. Queries consume cancellation through response bodies with a 30-second deadline; preview, mutation resolution and force refresh use 180 seconds and never retry automatically. Native EventSource observation has an independent lifetime.

AdminJobEffectsProvider in the administration layout owns the shared accepted-job observer. Service and extension adapters capture client, raw API identity, job ID and feature terminal effects inside the executing mutation. View disposal suppresses UI callbacks without losing accepted work. Provider/origin replacement retires old registrations, including A-to-B-to-A replacement. Terminal extension effects cancel one union of affected origin-scoped extension, service, integration/configuration, credential inventory, runtime and overview reads before invalidating them; inactive queries remain stale and unrelated API origins are untouched. Synchronous source edits and refresh also invalidate catalog/detail/installed snapshots and installed badges.

The private operator CLI uses these same generated operations with one isolated ApiClient per action. It reads API_URL → OPENMAPX_API_URL → http://localhost:3001 late, omits credentials and always attaches x-openmapx-local-admin from the trimmed optional OPENMAPX_LOCAL_ADMIN_TOKEN (empty when absent). It does not configure the global client or create cookies, Origin headers or a new authentication mode. Other operator commands retain their existing transport.

ext preview <idOrUrl> displays the complete validated inspected snapshot. Install/update preview again with their invocation client and selector before writing; port-bearing snapshots require an explicit matching --host-port-confirmation <fingerprint> supplied after human review. A mismatch requires new deliberate inspection. Generated functions own encoding exactly once, and omitted optional update/remove bodies stay absent. Reads use 30 seconds; preview and writes use finite 180-second deadlines through body consumption. Status/media/body/byte limits remain in the bound SDK transport. CLI failures use fixed safe messages and nonzero exits; validated 429 retains Retry-After without retrying. Timeouts are ambiguous and never cancel host jobs or trigger retries. The CLI prints queued 202 job IDs without adding an Activity watcher. Since install/update acquire another preview, a preceding explicit ext preview does not bypass the one-preview-per-30-seconds bucket; 429 ends that command without writing or automatically retrying.

Saved places application SDK​

The saved-places family covers all 14 session-authenticated /api/saved operations: list and place CRUD, labels, membership, import and export. Portable Zod contracts live under packages/core/src/api/contracts/saved; producer registration uses the shared HTTP contract registrar with session guards retained on the plugin. Authentication runs before application payload validation.

Import generated functions from @openmapx/core/saved-places-sdk and unwrap their validated status envelopes with unwrapSavedResponse. Creation and update replies are receipts, not full read records. Saved read types are inferred from the canonical schemas. Keep React Query keys, session gating and invalidation in the feature hooks, and pass query cancellation signals to acquisition.

Exports return consumed Blob values. The transport validates the media type against the requested GPX/GeoJSON/KML selector and requires attachment metadata. It consumes the UTF-8 response inside the application's cancellation/deadline lifetime, with a 40 MiB default download budget adjustable through request options. The web download adapter supplies a 60-second default deadline. Never return an unread response after releasing that lifetime or parse an XML download as JSON. Captured field attribution, licence bindings and import/export semantics remain part of the contract.

Run pnpm openapi:generate, pnpm sdk:generate, pnpm check-openapi and pnpm check-api-sdk for contract changes. Existing metadata and administration artifacts are generated independently and should not change when this family is updated. Use actual serialized Fastify responses through the generated SDK in addition to hook tests; the migrated PostgreSQL saved suite qualifies persisted read shapes, import concurrency/rollback, receipt invalidation and ownership.

Personal garage SDK​

The eight /api/vehicles and /api/parking operations share canonical contracts and named functions from @openmapx/core/garage-sdk. Vehicle creation has a separate receipt without persisted timestamps; read records and parking upserts have validated ISO timestamps. Public record projection excludes owning account IDs. Successful SDK replies require Cache-Control: no-store.

Structural request validation runs after session authentication. Existing garage normalizers retain trimming, defaults, electric/fuel rules and the write-only parking-expiry horizon. Partial updates preserve omitted fields. Account locks, ownership checks, default promotion, caps and database constraints stay in the producer. Core hooks retain identity-scoped local/remote query behavior; GarageSyncBridge consumes validated replies before clearing local rows and retires acquisition when its identity changes.

Run pnpm openapi:generate and pnpm sdk:generate after contract changes, and commit both outputs. The exact-artifact gate protects previous generated families.