The headless CMS that doesn't want your content. We provide the editing experience. Your S3 bucket stores the data. No vendor lock-in. No content hostage. Just JSON blobs you own.
Traditional headless CMSes removed the frontend but kept the content on their own infrastructure. Blobify publishes into storage you control.
We charge for the editing experience and user management - not for storing or delivering your content.
Your content lives in S3 or Cloudflare R2 buckets that you own and control. We never store your content on our servers.
Generous free tiers:
Most content sites fit entirely within these free tiers.
Optional content prefix (advanced)
You can add a per-space content prefix to add an extra folder segment for content JSON, summaries, indexes, and the asset catalog JSON, making direct bucket URLs harder to guess.
Asset files still stay in assets/media/.
Path hardening only. If your app fetches content JSON in the browser, the prefix can still be visible in network requests. SSR/ISR keeps it less obvious.
A complete content platform — from modeling to delivery.
Define models for your content types and reusable blocks like Hero, CTA, or FAQ
AST-based editor with embedded blocks, links, and references
Multi-language content with per-locale publishing
Link content together with validated cross-model references
Track changes and restore previous versions
Preview changes before going live
Images and files with metadata and transforms
Generated types and typed client from your schema
Cloudflare Images for resize, crop, and format conversion
Notify external services when content changes
Roles and per-space access control
From raw JSON files in your bucket to fully resolved content in one SDK call.
Each model gets summary shards with only the fields you pick. One manifest plus shard fetch gives you everything needed for list pages, search indexes, and sitemaps.
{
"items": [
{ "id": "c4f2e8a1", "slug": "hello-world", "title": "Hello World" },
{ "id": "d7a1c3b9", "slug": "getting-started", "title": "Getting Started" },
{ "id": "e9b4d2f7", "slug": "advanced-tips", "title": "Advanced Tips" },
// ... 512 more items
]
}Full content documents with localized fields, rich text, and references. All in one JSON file per item. Your app fetches these only for detail pages.
{
"id": "c4f2e8a1",
"model": "article",
"fields": {
"title": { "en": "Hello World", "de": "Hallo Welt" },
"slug": { "en": "hello-world", "de": "hallo-welt" },
"content": {
"en": {
"type": "root",
"children": [
{ "type": "heading", "depth": 1,
"children": [{ "type": "text", "value": "Hello, world!" }] },
{ "type": "paragraph",
"children": [
{ "type": "text", "value": "This is an " },
{ "type": "text", "value": "example", "marks": ["bold"] },
{ "type": "text", "value": " richtext." }
] }
]
}
},
"heroImage": { "type": "asset", "assetId": "img_a1b2" },
"author": { "model": "author", "id": "auth_x7y8" }
}
}URL patterns defined per model with per-locale overrides. English gets /blog/:slug, German gets /de/blog/:slug.
{
"article": {
"path": "/:locale/blog/:slug",
"locales": {
"en": "/blog/:slug"
}
},
"page": {
"path": "/:locale/:slug",
"locales": {
"en": "/:slug"
}
}
}One SDK call triggers parallel fetches. Summary manifests, shards, content, and assets are loaded concurrently and cached. Dependent summaries (for references) are fetched automatically. Subsequent calls hit the cache.
const article = await cms.findOne('article', { slug: 'hello-world' }, 'en');
// Behind the scenes:
// 1. Fetch summary manifest + shards (cached after first call)
GET /summaries/published/index.json // shard discovery
GET /summaries/published/article/s000.json // article summary shard
// 2. Find by slug in merged summary → id: "c4f2e8a1"
// 3. Fetch content + assets in parallel
GET /content/article/c4f2e8a1/published.json // full document
GET /assets/catalog.json // asset URLs (logical key)
// when contentPrefix is enabled:
// GET /_blobify/{prefix}/assets/catalog.json
// 4. Resolve references (fetches dependent summaries)
GET /summaries/published/author/s000.json
// 5. Resolve asset refs → URL strings
// 6. Return resolved contentThe SDK resolves references automatically. Asset references become URL strings from your bucket, content references become resolved summary entries. Use localize() and resolveUrl() helpers for the rest.
const article = await cms.findOne("article", { slug: "hello-world" }, "en");
console.log(article);
/* Console output
{
id: "c4f2e8a1",
model: "article",
fields: {
title: { en: "Hello World", de: "Hallo Welt" },
slug: { en: "hello-world" },
content: {
en: {
type: "root",
children: [
{ type: "heading", depth: 1, children: [{ type: "text", value: "Hello, world!" }] },
{
type: "paragraph",
children: [
{ type: "text", value: "This is an " },
{ type: "text", value: "example", marks: ["bold"] },
{ type: "text", value: " richtext." }
]
}
]
}
},
heroImage: "https://cdn.example.com/media/img_a1b2.jpg",
author: {
id: "auth_x7y8",
fields: { name: "Jane Smith" }
}
}
}
*/
resolveUrl("article", article, "en") // "/blog/hello-world"
resolveUrl("article", article, "de") // "/de/blog/hello-world"
localize(article.fields.title, "en") // "Hello World"
localize(article.fields.title, "de") // "Hallo Welt"Content lives on S3 - fetch it however you want. Or use our typed client with full autocomplete for your content types and fields.
// No API needed - fetch directly from your bucket
const manifest = await fetch('.../summaries/published/index.json').then(res => res.json());
const shards = await Promise.all(
manifest.models.article.shards.map(shard =>
fetch('.../summaries/published/article/' + shard + '.json').then(res => res.json())
)
);
const articles = shards.flatMap(shard => shard.items);
articles.forEach(article => console.log(article.fields.title.en, article.fields.slug.en));import { createClient } from './blobify';
const cms = createClient({
baseUrl: 'https://my-bucket.s3.amazonaws.com',
spaceId: 'main',
});
// Full TypeScript autocomplete
const articles = await cms.getAll('article', 'en');
const page = await cms.findOne('page', { slug: 'about' }, 'en');Automatic image optimization via Cloudflare Images. Resize, convert to WebP/AVIF, and serve from the edge - all with a simple helper function.
import { cfImage } from './blobify';
// Transform images via Cloudflare Images
const optimized = cfImage(article.fields.heroImage, {
width: 800,
height: 600,
quality: 85,
format: 'webp'
});
// Output: https://your-site.com/cdn-cgi/image/w=800,h=600,q=85,f=webp/...// Typed client fetches summary manifests and shards once, builds hash maps
const cms = createClient({
baseUrl,
spaceId: 'main',
});
// Find by any summary field (slug, category, etc.)
const article = await cms.findOne(
'article', { slug: 'my-post' }, 'en'
);
const frontpage = await cms.findOne(
'page', { isFrontpage: true }, 'en'
);
const techPosts = await cms.findMany(
'article', { category: 'tech' }, 'en'
);
// References resolved automatically (O(1) lookup)
article.fields.author // → Author object
article.fields.heroImage // → { url, alt, width, … }References and assets are resolved automatically from summaries. The typed client fetches summary manifests and shards at build time, then resolves IDs instantly with O(1) lookups.
Define URL patterns directly in your model schema. Support for per-locale overrides means German URLs can use German paths while English uses English.
// Define routes in your model schema
{
"model": "article",
"routing": {
"path": "/:locale/blog/:slug",
"locales": {
// en without the locale prefix
"en": "/blog/:slug"
}
}
}
// Resolve URLs with the typed client
import { resolveUrl } from './blobify';
resolveUrl('article', article, 'en');
// → "/blog/hello-world"
resolveUrl('article', article, 'de');
// → "/de/blog/hello-world"Pure usage pricing. Start free, then scale by team, content volume, and write activity.
Adjust each slider to match your needs.
Did you know?
We don't charge for requests, storage, or bandwidth. Content lives in your own S3/R2 bucket — you pay your cloud provider directly at their rates.
For teams with strict compliance and very high write volume.