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

StatusCodeWhen
401API key requiredMissing X-API-Key
401Invalid API keyWrong or deactivated key
403insufficient_scopeKey lacks required scope
403API key cannot access this projectKey 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-01

04 SDK: @eoxscriptum/client

Branch-aware edge client

createClient options

OptionTypeDefaultRequired
projectIdstringyes
tokenstringyes
endpointstringhttps://api.eoxscriptum.comno
branchBranch"main"no
fetchtypeof fetchglobalThis.fetchno

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

TypeDescription
textSingle-line text
richtextTipTap rich text editor
markdownMarkdown source
numberNumeric value
booleanToggle
dateDate picker
datetimeDate + time
selectSingle choice
multiselectMultiple choice
tagsFree-form tags
imageImage (R2)
mediaAny file (R2)
urlURL
emailEmail
jsonRaw JSON
slugURL-safe slug
colorColor picker
referenceCross-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.

SvelteTypeScriptSSRRunes

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.