Required for the site to work (login, preferences, security). Always on.
Why we built Osprey, our headless CMS
Osprey keeps content schemas in TypeScript, ships published content at build time, and returns a 404, not a permission error, for anything private.
Most headless CMS options force a choice: a visual-editor-first tool that treats the schema as a database blob it generated once, or a hand-built setup where every page load makes a live call back to the CMS. We built Osprey, our headless CMS, to remove that choice for teams that want a content schema versioned like code and a site that ships as static files. Here is what the code actually does, not just what the pitch says.

Schemas are code, not a database blob
In Osprey, a collection is defined in TypeScript and lives in the same repository as the rest of the application. That means the same version control, code review and end-to-end types a team already applies to its application code apply to the content model too. The admin interface is a convenience for editing entries; it is never the source of truth for the schema itself.
Content ships at build time, not on every request
The @osprey/astro loader fetches published entries from Osprey's public REST endpoint while the site builds, and hands them to Astro's content collections, with no API key required for that public read. The deployed site is plain static HTML: Osprey is a build-time dependency, not a runtime one. If a build runs while the CMS is slow or unreachable, the loader keeps the previously cached entries instead of failing.
Private by default, invisible when denied
A collection is exposed on the public API only when its own access.read rule explicitly admits an anonymous caller; anything without that rule stays private. A request against a private collection, or for a document that is not published, comes back as a 404 rather than a permission error, so the API never confirms that a private collection exists in the first place.
SEO metadata travels with each entry
Every entry the public API returns carries a merged SEO object: the entry's own title, description, Open Graph and Twitter tags and canonical URL take priority, and workspace-wide defaults fill in whatever it does not set. The same payload includes the resolved robots directive and hreflang links to every published translation of that page, so a frontend can render its meta tags without rebuilding that logic itself.
One deployment, many tenants
Osprey is multi-tenant: a single deployment can serve more than one site, each scoped to its own tenant. On the public content routes, which tenant to read from is resolved from the request's own host header rather than trusted from a client-supplied one, so an anonymous request cannot read another tenant's content by sending a different tenant id. Every database query the API runs is scoped to a tenant internally, not left to each handler to remember.
GDPR mechanics, not just a claim
Osprey ships a right-to-be-forgotten endpoint that redacts personal data, including form submissions, audit-log actors and user accounts, for a given email or id, scoped to one tenant. A separate export endpoint produces a full manifest of a tenant's content for data-portability requests. The bundled cookie-consent manager treats only strictly necessary cookies as always-on and defaults every other category to denied until a visitor opts in, with "reject all" given the same weight as "accept all". Because Osprey is self-hosted, a team can also run it entirely on EU-based infrastructure; dedicated EU data-residency guarantees are part of the product's enterprise plan rather than a default of every install.
Headless by design
Osprey stores content and serves it as JSON; it has no opinion about how a site looks. A team can build the frontend in Astro, Next.js, or any static-site generator that can call a REST API, and use Osprey purely as the content layer behind it. The current plans, including the self-hosted and enterprise tiers, are listed on the Osprey product page.
What to do
- Define your content model as TypeScript collections and commit it alongside your application code.
- Set an explicit
access.readrule on every collection you want the public API to serve; leave it unset to keep the collection private. - Add the
@osprey/astroloader, or call the REST API directly, so content is pulled in at build time rather than on every request. - Set per-entry SEO fields only where they need to differ from your site-wide defaults, and let the defaults fill the rest.
- Use the built-in export and right-to-be-forgotten endpoints for data-portability and erasure requests instead of building your own.
- If EU-only hosting matters for your compliance posture, self-host on EU infrastructure or ask about the enterprise plan's residency guarantees.
Sources: Osprey product page, getosprey.dev.