CMS
The Ovok CMS holds the content your app needs that FHIR does not model: legal pages, UI translations, release notes and content types you define yourself. You write it once in the Ovok Console, and your apps read the published version over a public, cacheable API.
What you can read
| Collection | What it holds | Who may read it |
|---|---|---|
legal-pages | Terms, privacy and similar pages, with a localised title and body. | Anyone who names your tenant code. |
translations | UI text, one document per group, with a localised value for each key. See Use Ovok CMS with i18n. | Anyone who names your tenant code. |
release-notes | Release notes, with tags and a publish date. | A CMS API key, or a signed-in user of your project. |
| Your own content types | Items you define in the Console. | A CMS API key, or a signed-in user of your project. |
Ovok decides which collections are open to anyone; an operator can change that list for a deployment.
The routes
| Route | Page |
|---|---|
GET /v1/public/cms/collections/:collection/items | List the items of a collection |
GET /v1/public/cms/collections/:collection/items/:slug | Get an item by slug |
GET /v1/public/cms/:contentTypeSlug/items | List the items of a content type |
| Use Ovok CMS with i18n |
Enable the CMS
Enable the CMS for your project in the Ovok Console. The Console sets the CONTENT_ENABLED marker, provisions the CMS environment, and manages authoring access and billing. Ovok's public-delivery routes do not read the setting itself. Changing the setting directly through the project settings API is not a substitute for the Console workflow; public delivery also depends on the project's CMS environment.
Your project's content space is created the first time the CMS is used, by the Console or by an app reading content. It needs your project's tenant code to exist, and a brand-new code can be refused as unknown for up to a minute.
Tenants and environments
Content belongs to a tenant, which is your project, named by its tenant code, and to an environment: dev, staging or prod. Each environment has its own content, so write the content in the same environment the app reads.
| You send | Where | Meaning |
|---|---|---|
tenant | Query, or the x-ovok-tenant-code header | Your tenant code. |
projectId | Query | Your project's id, a UUID. Use it when you do not have the tenant code. A tenant code takes precedence if both are sent, but the project ID must still be a valid UUID. |
environment | Query, or the x-ovok-environment header | dev, staging or prod. |
- Name the tenant on every read. A signed-in user may leave it out and reads their own project. Everyone else who names nothing gets
400. - The environment is optional, and a wrong value is not an error. Anything other than
dev,stagingorprodis ignored, and the request uses the host's default:prodon the production API,stagingon the sandbox. Check the spelling:?environment=productionsilently reads the default. - Headers beat query values, even empty ones. An empty
x-ovok-tenant-codeheader hides?tenant=.
Who can read content
Three ways in, tried in this order:
| Way | How | Reads |
|---|---|---|
| A CMS API key | x-api-key: <key>, Authorization: Bearer <key> or ?apiKey=<key> | Published content of the tenant you name. |
| An open collection | Nothing. Only the tenant code. Applies to legal-pages and translations by default, and only on the collection routes. | Published content of the tenant you name. |
| A signed-in session | An Ovok access token as Authorization: Bearer <token> | Published content of the user's own tenant only. |
Anything else answers 401.
- There is one CMS key per Ovok deployment, not one per project. Ask Ovok for it. It reads the published content of any tenant, so keep it on your server and do not ship it in a public client; use an open collection or a signed-in session there.
- A session that names another tenant gets
403, not401, so the SDKs do not sign the user out. The exception is an open collection: a signed-in user can read another tenant'slegal-pagesortranslationsby naming it. - Authentication comes before everything else.
GET .../collections/users/itemswithout a key answers401, even though no such collection is served.
Languages
The CMS has four languages: de, en, fr and es. en is the default.
- Pass
locale=de. A tag that is not a valid language tag answers400:en_US,DEandallare refused. - Use the bare codes. A tag such as
de-DEis accepted, but the CMS does not have it and answers in the default language, English. The Ovok SDKs mapde-DEtodefor you. - A text or textarea field with no value in the language falls back to English field by field.
Published content
Only documents with status published are returned. A draft, and a document that does not exist, look the same: not found. There are no versions or scheduled publishing: publishedAt is an ordinary date and hides nothing.
Rich text is returned as the editor's JSON structure, not as HTML.
Shared content
Ovok keeps a shared tenant of its own, which contributes documents to every tenant:
- On the list routes, its published documents are merged with yours.
- On the get-by-slug route, it is the fallback when your tenant has no document with that slug.
- Your document wins when the slug is the same. A
translationsgroup of yours replaces the shared group of the same slug whole, so a group with one key hides the shared group's other keys. - The merge is skipped when either side has more than 200 published documents, and your own list is returned unmerged.
- A failing shared read never breaks your read: you get your own documents.
Caching
| Response | Cache-Control |
|---|---|
| A tenant named by code or project id | public, s-maxage=60, stale-while-revalidate=300 |
| A signed-in user who names no tenant | no-store |
| An error | None |
A tenant-named response also carries a Vary header on the tenant, project, environment and API key headers. Ovok caches each read for about 60 seconds, so an edit shows up within a minute, plus up to about 5 minutes more behind a CDN. Nothing purges the cache when you publish.
Browsers and CORS
For an allowed origin, Ovok's CORS policy accepts Content-Type, Authorization, Accept-Language, If-Match, X-Medplum, X-Tenant-Code and the tracing headers. x-api-key, x-ovok-tenant-code and x-ovok-environment are not allowed, so a browser must send:
- the key as
Authorization: Bearer <key>or?apiKey=; - the tenant and environment as
?tenant=and?environment=.
A key in the URL can end up in logs. Prefer the header from a server.
Limits
| What | Value |
|---|---|
page | 1 to 100. A non-number falls back to 1. |
limit | 1 to 50. Default 20. |
| Documents readable per list | 5,000 (50 per page, 100 pages) |
| Content type route | At most 100 items, no paging, no sort, no locale |
| Slug | Lower-case letters, digits, -, _ and .; starts with a letter or digit; up to 128 characters |
| Rate limit | Key-only and unauthenticated calls use the unknown-caller limit: 1,000 per minute per route and IP address. Signed-in calls use the caller's role limit per route: patients and related people 100 per 30 seconds; practitioners 200 per 30 seconds; admins and client applications 1,000 per 30 seconds. Over a limit returns 429. |
Errors
| Status | message | Cause |
|---|---|---|
400 | Public CMS delivery requires a tenant code, a project id, or a signed-in session | No tenant is named and the caller is not signed in. |
400 | Unknown tenant code: <code> | The tenant code does not exist. |
400 | Invalid project id. followed by what was expected | projectId is not a UUID. |
400 | Invalid locale "<value>". Expected a language tag like "de" or "en-US". | The language tag is malformed, or is all. |
400 | Invalid sort "<value>". Expected a field name, optionally prefixed with "-". | sort is not a top-level field name. |
400 | Environment "<env>" is suspended for this project | The environment has been suspended. Content is not deleted. |
401 | Invalid or missing CMS public API key | The collection is not open and there is no valid key or session. |
401 | CMS public API key is not configured | The deployment has no key, and the request needs one. |
403 | A signed-in session reads its own tenant's content | A signed-in user named another tenant. |
404 | Not found | A missing, unpublished or malformed slug, or a collection that is not served. |
429 | Too Many Requests | Over the rate limit. |
502 | Unable to resolve the CMS tenant | The CMS is unreachable. Retry. |
Errors share the format described in errors and troubleshooting: statusCode, error, timestamp, path, message and requestId. A CMS failure is never answered with an empty 200.