Developer Documentation
EOXScriptum Docs
Everything you need to integrate the content runtime into your stack. SDK reference, REST API, framework guides, and architecture deep-dives.
01 Quickstart
Get content on screen in 60 seconds
1. Install
npm install @eoxscriptum/client 2. Initialize
import { createClient } from "@eoxscriptum/client";
const cms = createClient({
projectId: "your-project",
token: "eox_...",
}); 3. Fetch
const { document } = await cms.get("post", { slug: "hello-world" });
const { items } = await cms.list("post", { limit: 10 }); const { posts } = await (await fetch(
"https://api.eoxscriptum.com/v1/my-project/posts",
{ headers: { "X-API-Key": "eox_..." } }
)).json(); Or skip the SDK entirely. One fetch. Content from 330+ edge locations.
02 Authentication
Two auth models
API Key (public reads)
- Header:
X-API-Key: eox_... - Or query:
?api_key=eox_... - Scoped to a project, read-only by default
- Get one from the dashboard wizard or Settings > API Keys
JWT Bearer (admin writes)
- Header:
Authorization: Bearer <jwt> - Issued via OTP (
POST /auth/otp/request+/auth/otp/verify) - Or magic link (
POST /auth/magic-link/request+/auth/magic-link/verify)
Error responses
| Status | Code | When |
|---|---|---|
| 401 | API key required | Missing X-API-Key |
| 401 | Invalid API key | Wrong or deactivated key |
| 403 | insufficient_scope | Key lacks required scope |
| 403 | API key cannot access this project | Key scoped to different project |
03 Content API
Read content from the edge
Single document
GET /v1/:project/content/:type/:slug?branch=main
Header: X-API-Key: eox_... Response shape:
{
"document": {
"id": "abc123",
"title": "Hello World",
"slug": "hello-world",
"excerpt": "...",
"body": { "type": "doc", "content": [] },
"bodyHtml": "<p>...</p>",
"featuredImage": "covers/project/hero.webp",
"author": "writer-1",
"tags": ["tutorials"],
"seoTitle": "Hello World",
"seoDescription": "...",
"ogImage": null,
"publishedAt": "2026-05-19T00:00:00Z",
"revision": 3,
"updatedAt": "2026-05-19T12:00:00Z"
},
"branch": "main",
"type": "post",
"slug": "hello-world",
"version": 3
} Collection list
GET /v1/:project/content/:type?branch=main&limit=20&cursor= Response:
{
"items": [{ "id": "...", "title": "...", "slug": "...", ... }],
"cursor": "next-id-or-null",
"branch": "main",
"type": "post"
} Legacy posts API
GET /v1/:project/posts?page=1&limit=20&tag=tutorials&search=hello&sort=publishedAt:desc&fields=title,slug,excerpt
GET /v1/:project/posts/:slug Schema introspection
GET /v1/:project/schema Response:
{
"project": "my-project",
"contentTypes": [{
"slug": "post",
"name": "Blog Post",
"schema": {
"version": 1,
"fields": [...],
"editor": { "layout": "single" }
}
}]
} Feeds
GET /v1/:project/feed.json
GET /v1/:project/feed.xml?limit=20&since=2026-01-0104 SDK: @eoxscriptum/client
Branch-aware edge client
createClient options
| Option | Type | Default | Required |
|---|---|---|---|
| projectId | string | — | yes |
| token | string | — | yes |
| endpoint | string | https://api.eoxscriptum.com | no |
| branch | Branch | "main" | no |
| fetch | typeof fetch | globalThis.fetch | no |
client.get()
const { document, version } = await cms.get<BlogPost>("post", {
slug: "hello-world",
branch: "main",
}); client.list() with cursor pagination
const page1 = await cms.list<BlogPost>("post", { limit: 10 });
// Next page:
const page2 = await cms.list<BlogPost>("post", {
limit: 10,
cursor: page1.cursor,
}); client.live()
const sub = cms.live<BlogPost>("post", {
slug: "hello-world",
branch: "draft",
});
sub.on("change", (next) => console.log(next));
// Cleanup:
sub.close(); Requires yjs as a peer dependency for live subscriptions.
Error handling
import { EoxNotFoundError, EoxAuthError } from "@eoxscriptum/client";
try {
const doc = await cms.get("post", { slug: "missing" });
} catch (err) {
if (err instanceof EoxNotFoundError) { /* 404 */ }
if (err instanceof EoxAuthError) { /* 401/403 */ }
}05 SDK: @eoxscriptum/svelte
Svelte 5 runes adapter
npm install @eoxscriptum/svelte @eoxscriptum/client useDocument
<script>
import { useDocument } from "@eoxscriptum/svelte";
import { cms } from "$lib/cms";
let { data } = $props();
const doc = useDocument({
client: cms,
type: "post",
slug: data.slug,
initial: data.post,
});
</script>
{#if doc.loading}
<p>Loading...</p>
{:else if doc.error}
<p>Error: {doc.error.message}</p>
{:else}
<h1>{doc.current?.title}</h1>
{@html doc.current?.bodyHtml}
{/if} useLive
<script>
import { useLive } from "@eoxscriptum/svelte";
import { cms } from "$lib/cms";
let { data } = $props();
const live = useLive({
client: cms,
type: "post",
slug: data.slug,
branch: "draft",
initial: data.post,
});
</script>
<article>
<h1>{live.current?.title}</h1>
{@html live.current?.bodyHtml}
</article>06 Content Types
Schema-driven content modeling
Content types define the shape of your documents. Each project starts with types seeded from a template (Developer Blog, Digital Magazine, etc.) and can be customized via the admin dashboard.
Field types
| Type | Description |
|---|---|
| text | Single-line text |
| richtext | TipTap rich text editor |
| markdown | Markdown source |
| number | Numeric value |
| boolean | Toggle |
| date | Date picker |
| datetime | Date + time |
| select | Single choice |
| multiselect | Multiple choice |
| tags | Free-form tags |
| image | Image (R2) |
| media | Any file (R2) |
| url | URL |
| json | Raw JSON |
| slug | URL-safe slug |
| color | Color picker |
| reference | Cross-type relation |
07 Caching
Sub-5ms reads at 330+ edge locations
All content reads are served from Cloudflare KV at the nearest point of presence. The cache layer is transparent and event-driven.
- Default headers:
Cache-Control: public, max-age=60, s-maxage=300 - Cache status:
X-Cache: HIT | MISS | BYPASS - Event-driven invalidation on publish, update, and delete
- Cache bypass: add any custom query param (tag, search, sort, fields)
- Branch-aware cache keys: different branches never collide
08 Media
Images and assets on R2
- Upload via dashboard drag-and-drop or
POST /admin/projects/:id/media - Stored in Cloudflare R2 with immutable cache headers (1 year)
- Serve URL:
https://api.eoxscriptum.com/media/:key - Bulk generation: "Generate 10 images" creates AI-generated project-specific images via Replicate
09 Framework Guides
Integrate with any stack
These match the framework choices in onboarding. Pick one guide for a complete production-ready path, or use Other / REST for any language runtime.
SvelteKit
Server-first SvelteKit integration using private env vars, load functions, typed SDK reads, metadata, and optional live previews.
Install and environment
Keep the API key server-only. Use the client package for read paths and the Svelte adapter only when you need client-side live state.
npm install @eoxscriptum/client @eoxscriptum/svelte
# .env
EOXSCRIPTUM_ENDPOINT=https://api.eoxscriptum.com
EOXSCRIPTUM_PROJECT=my-project
EOXSCRIPTUM_API_KEY=eox_...Create a server client
// src/lib/server/cms.ts
import { createClient } from "@eoxscriptum/client";
import { env } from "$env/dynamic/private";
export const cms = createClient({
endpoint: env.EOXSCRIPTUM_ENDPOINT,
projectId: env.EOXSCRIPTUM_PROJECT ?? "my-project",
token: env.EOXSCRIPTUM_API_KEY ?? "",
branch: "main",
});List posts with cursor pagination
// src/routes/blog/+page.server.ts
import { cms } from "$lib/server/cms";
export async function load({ url }) {
const cursor = url.searchParams.get("cursor") ?? undefined;
const { items, cursor: nextCursor } = await cms.list("post", {
limit: 12,
cursor,
});
return { posts: items, nextCursor };
}Render a single post and SEO metadata
// src/routes/blog/[slug]/+page.server.ts
import { error } from "@sveltejs/kit";
import { cms } from "$lib/server/cms";
export async function load({ params }) {
const result = await cms.get("post", { slug: params.slug }).catch(() => null);
if (!result?.document) throw error(404, "Post not found");
return { post: result.document, version: result.version };
}Page component
<script lang="ts">
let { data } = $props();
const post = data.post;
</script>
<svelte:head>
<title>{post.seoTitle ?? post.title}</title>
<meta name="description" content={post.seoDescription ?? post.excerpt} />
</svelte:head>
<article>
<h1>{post.title}</h1>
{@html post.bodyHtml}
</article>Preview and live editing
Use branch=draft for preview routes and @eoxscriptum/svelte useLive for collaborative preview screens that can tolerate a browser-side subscription token.
Ship checklist
- Store EOXScriptum variables in private env, never $env/static/public.
- Return only published content from public routes unless you intentionally pass branch=draft.
- Style post HTML with a scoped prose class or article stylesheet.
- Use cursor pagination for large archives.
10 Feeds
JSON Feed and RSS
GET /v1/:project/feed.json
GET /v1/:project/feed.xml?limit=20&since=2026-01-01 Both endpoints require API key authentication. The JSON Feed follows the jsonfeed.org spec. The RSS feed follows the Atom format.