Conventions
1. Transport, paths, media types
Section titled “1. Transport, paths, media types”| Item | Rule |
|---|---|
| Base path | /api/v1 (major in path). Ops endpoints unversioned: /health, /ready, /metrics |
| Self-description | GET /api/v1/openapi.json, Swagger UI GET /api/v1/docs, GraphQL POST/GET /api/v1/graphql (GraphiQL off by default), WS GET /api/v1/ws, socket.io path /socket.io/ namespace /v1 |
| JSON | application/json, UTF-8, no BOM; camelCase field names; unknown fields ignored by readers |
| Errors | application/problem+json (RFC 9457) |
| Partial update | PATCH with application/merge-patch+json (RFC 7396); PUT replaces |
| Actions | verbs as sub-resources: POST …/players/{playerId}/play; long actions return 202 + Location: /api/v1/jobs/{jobId} |
| Time | RFC 3339 UTC with ms (2026-10-19T12:00:00.000Z); durations / positions in seconds (float) |
| Units | metres, degrees, seconds, Hz; brightness 0..1; volume 0..1 linear gain with optional gainDb; colours #rrggbb |
| i18n text | { "en": "…", "ru": "…" } (BCP 47 keys), as in .dome meta.title |
2. Identifiers
Section titled “2. Identifiers”- Server-owned resources: UUIDv7, lowercase string (
0192…), sortable by creation time. - Document-scoped ids (from
.dome:displays[].id,assets[].id, playlist / cue ids):^[A-Za-z0-9._-]{1,64}$, unique within their parent; addressed as nested path segments (/venues/{venueId}/displays/p1). - Aliases:
default= the installation’s default venue;main= default media player. Clients that know only one dome never need a venue id. - Client-chosen ids allowed on
PUTcreate (idempotent) for venues / shows imported from.dome(meta.id).
3. Errors (RFC 9457)
Section titled “3. Errors (RFC 9457)”{ "type": "https://freedome.nativemind.us/problems/engine-unavailable", "title": "Visual engine unavailable", "status": 503, "detail": "freedome-engined not reachable at unix:/run/freedome/engine.sock", "instance": "/api/v1/venues/default/players/main/play", "code": "engine-unavailable", "traceId": "4bf92f35…", "errors": [ { "pointer": "/position", "detail": "must be ≥ 0" } ] }code is the stable machine key (also the last path segment of type); the same object is the data of WS
error messages, the socket.io ack error, and GraphQL errors[].extensions.problem. Catalogue (extensible, in
schemas/common/1.0/problem.schema.json): validation-failed 422, not-found 404, conflict 409,
precondition-failed 412 (ETag), precondition-required 428, unauthenticated 401, forbidden 403 (missing
scope; detail names it), rate-limited 429 (+Retry-After), payload-too-large 413, unsupported-format 415,
engine-unavailable 503, engine-incompatible 503, degraded 503, job-failed (in job results),
upload-checksum-mismatch 422, legacy-unsupported 501, internal 500.
4. Pagination, filtering, sorting
Section titled “4. Pagination, filtering, sorting”- Cursor pagination on every collection:
?limit=(default 50, max 500)&cursor=(opaque). Response{ "items": [...], "nextCursor": "…" | null }+Link: <…>; rel="next". No total counts by default (?count=truewhere cheap). - Filters as plain query params named after fields (
?type=video&tag=intro),?q=full text where supported,?sort=field,-other. GraphQL uses Relay connections (first,after,edges,pageInfo).
5. Concurrency, idempotency, caching
Section titled “5. Concurrency, idempotency, caching”- Every resource representation carries
ETag(strong, derived from arevisioninteger also present in the body).PUT/PATCH/DELETEon venues, shows, playlists, timelines, cues, layouts, config requireIf-Match(428 otherwise); 412 on mismatch. Playback actions do not (last writer wins, ordered by server receive time). Idempotency-Keyheader (UUID, 24 h retention, per principal) on all non-idempotentPOSTs (create, actions, jobs, uploads creation). Replay returns the original status and body; different body with the same key → 409. WS / socket.iocallmessages use the envelopeidas the idempotency key.GETsupportsIf-None-Match→ 304. Asset / export downloads supportRange(206) andETag= sha256.
6. Authentication and scopes
Section titled “6. Authentication and scopes”| Mechanism | Used by | Notes |
|---|---|---|
Authorization: Bearer <access JWT> |
users (Manager, Sphere) | from POST /auth/session (local account) or an L7 token exchanged at POST /auth/token-exchange when the installation is linked to the cloud; 15 min access + rotating refresh |
Authorization: Bearer fdk_<key> |
services, kiosks, gateways | API keys, scoped, revocable, shown once |
| Pairing | ecosystem apps (guest) | POST /auth/pairings → 6-digit code shown in Manager / on the dome; operator approves → app gets a guest token; QR = freedome://pair?host=…&code=… |
| Anonymous | GET /api/v1/info, /health, /ready |
configurable: an installation may allow anonymous guest (kiosk mode) |
| OSC / serial / raw TCP | controllers | no tokens; per-listener CIDR allowlist mapped to a fixed scope set |
Scopes (roles = scope bundles; from flutter_freedome FreeDomeUserRole / FreeDomePermission,
models/dome_models.dart:50-65):
| Scope | guest | operator | admin | superadmin |
|---|---|---|---|---|
info:read, events:subscribe, playback:read, content:read |
✓ | ✓ | ✓ | ✓ |
playback:control (play / pause / volume / brightness), apps:publish |
✓ (if venue allows) | ✓ | ✓ | ✓ |
content:write (assets, shows, uploads, imports) |
✓ | ✓ | ✓ | |
audio:control, displays:control (patterns, power) |
✓ | ✓ | ✓ | |
calibration:run, audio:calibrate, venues:write |
✓ | ✓ | ||
gateways:manage, system:read, users:manage, keys:manage |
✓ | ✓ | ||
system:admin (reboot, update, restore, engine restart, config) |
✓ |
Every OpenAPI operation declares security with its scope; GraphQL fields carry @auth(scope: "…"); WS /
socket.io / OSC operations are checked against the same table via parity.yaml.
7. Rate limits and sizes
Section titled “7. Rate limits and sizes”RateLimit-Policy / RateLimit headers (IETF draft) on 429; defaults per principal configurable. Body limit for
JSON 4 MB; media go through uploads — the current 2 MB body limit and lost extension
are replaced.
8. How this repository enforces the conventions
Section titled “8. How this repository enforces the conventions”| Convention | Enforced by |
|---|---|
camelCase, unique operationId |
.spectral.yaml freedome-operation-id-camel-case, operation-operationId-unique |
security + scope on every operation |
freedome-operation-security: security present (anonymous = [{}]) and x-scope = anon | any | <area>:<action> |
errors are application/problem+json |
freedome-problem-json (every 4xx, 5xx, 4XX, 5XX, default response); Redocly operation-4xx-problem-details-rfc7807 |
limit / cursor on collections |
freedome-collection-pagination (GET with x-collection: true or a 200 body {items, nextCursor}) |
| examples on every JSON body | freedome-examples (requests and 2xx responses) |
If-Match on PUT / PATCH / DELETE |
freedome-if-match; playback-style actions opt out with x-concurrency: none |
Idempotency-Key on POST |
freedome-idempotency-key; idempotent-by-nature POSTs (e.g. /auth/session) opt out with x-idempotency: none |
JSON Schema 2020-12, $id = published URL |
tools/schemas/test.mjs ($id must equal https://freedome.nativemind.us/schemas/<path>) |
| licence header | tools/hygiene/licence-check.mjs (NativeMindNONC in the first 5 lines; schemas: top-level $comment) |
Negative fixtures for each Spectral rule: tools/tests/spectral/ (make lint-tests).