Everything it does, on one page.

Verified in the code that runs it, on 2 September 2026. Nothing here is planned or coming soon.

The schema is the form.

What you write on the left is what editors get on the right.

  • fields.text()
  • fields.textarea()
  • fields.richText()
  • fields.number()
  • fields.boolean()
  • fields.datetime()
  • fields.slug()
  • fields.select()
  • fields.relation()
  • fields.media()
  • fields.group()
  • fields.array()
  • fields.json()
  • fields.blocks()
Flags, not screens
required and localized are two booleans on the field.
Slug from the title
fields.slug({ from: 'title' }) fills itself; select carries fixed options.
Also from the admin
Custom collections can be created in the admin and stored in the database.

One content, four ways to read it.

Pick the one your stack already speaks.

Public entries need no key. Back-office documents need an API key or a bearer token.

shellHTTP
curl 'https://cms.altovar.net/api/v1/collections/blog-post/entries?locale=en'

{ "data": [ { "id": "fdoc_16d9i3ef", "locale": "en",
              "data": { "title": "EU-first content infrastructure", … },
              "seo":  { "metaTitle": …, "metaDescription": …, "robots": "index,follow" } } ],
  "pagination": { "page": 1, "limit": 20, "total": 3, "totalPages": 1 } }

# The back-office route is the same shape, with a key:
curl -H 'x-api-key: sk_…' \
     'https://cms.altovar.net/api/v1/collections/blog-post/documents?limit=10'

A typed client with the same envelope: data and pagination.

blog.tsTS
import { OspreyClient } from '@osprey/client'

const osprey = new OspreyClient({
  baseUrl:  'https://cms.altovar.net',
  tenantId: 'altovar',
})

const { data, pagination } = await osprey
  .collection('blog-post')
  .listEntries({ locale: 'en', limit: 10 })

Queries only, over POST. GraphiQL is available in development.

graphqlGQL
POST /api/v1/graphql
x-api-key: sk_…

query Posts($limit: Int) {
  documents(collectionSlug: "blog-post", status: "published", limit: $limit) {
    data { id status data }
    pagination { total }
  }
}

Entries land in Astro content collections at build time. No key on the edge.

src/pages/index.astroASTRO
---
import { getCollection } from 'astro:content'

const posts = (await getCollection('blog'))
  .filter((p) => p.data.locale === 'en')
  .sort((a, b) => b.data.publishedAt.localeCompare(a.data.publishedAt))
---
{posts.map((p) => (
  <article>
    <a href={'/' + p.data.slug}>{p.data.title}</a>
    <p>{p.data.excerpt}</p>
  </article>
))}
Read the OpenAPI document →

Publish here, appear there.

Drafts stay private. A published entry reaches the public endpoint within a minute, and a webhook can rebuild the site.

Three statuses
draft, published, archived. Pages add review, approval and scheduling.
Signed webhook
HMAC-SHA256 over the body in x-osprey-signature. Nine events, secret encrypted at rest.
Cache headers
Public reads carry max-age=60 with stale-while-revalidate, ready for any cache in front.

Ten languages, one matrix.

Every localized field, every locale, counted.

Content locales
en, it, es, de, fr, nl, pl, sv, pt, ro, set per workspace.
Admin
English and Italian, switched from the top bar.
Coverage
A matrix per page and locale, and the missing strings as a CSV.
Delivery
One query parameter picks the language; hreflang alternates travel with each entry.

Your domain, in four steps.

Through the API today. The settings page is on the way, so the page says so.

  1. Claim POST the host to the domains endpoint. It waits as pending.
  2. Prove Add the TXT record _osprey-verify.<host> with the token you got back.
  3. Verify POST again with verify: true. The host moves onto the workspace.
  4. Point CNAME to the platform. The certificate is issued on the first request.
domainsHTTP
POST /api/v1/admin/domains
Authorization: Bearer <jwt>
{ "host": "www.example.com" }

{ "status": "pending",
  "verification": { "type": "TXT",
                    "name": "_osprey-verify.www.example.com",
                    "value": "osprey-verify=…" } }

Custom domains depend on the plan or on the hosting add-on.

Control that shows its work.

Each line below is a route and a screen, not a roadmap.

Roles
admin, editor, author, contributor, viewer
API keys
hashed, shown once, read-only flag, expiry
Audit log
who, what, when and from where, for every state change
Revisions
page history with diff and restore
Trash
deleted pages wait; restore them or delete for good
Together
presence and shared page editing, field by field
Consent
cookie banner and a consent log with hashed IP
Erasure and export
one call redacts a subject; export from the admin or the CLI
Sign-in
OpenID Connect per workspace, or a local password
Rate limits
one bucket per route family, limits in the response headers

The pieces around it.

Two published packages, a CLI, an MCP server and a self-host stack.

@osprey/client
typed SDK, React block renderer, consent manager
@osprey/astro
content loader, SEO head, translations sync
CLI
setup, init, dev, build, generate-types, link, export
MCP server
sixteen tools over stdio, for the AI agent you already use
Self-host
one compose file and a first-run wizard at /setup
AI assist
translate gaps, fill SEO, write alt text; off until you switch it on

Your content. Your server. Today.

Free plan, no card. Hosted in the EU, or on your own machines.