How-to

How to Set Up a Headless CMS for Free

Set up a headless CMS for free in fifteen minutes. Schema, API key, fetch call, and webhook, plus the free-tier limit you will hit first.

7 min read
Pipeline diagram of a free headless CMS setup: typed field slots, an API key, a delivery endpoint, and a rendered page, under a mostly empty usage gauge

You can set up a headless CMS for free in about fifteen minutes, and you have two routes. Self-host an open-source one like Strapi or Payload, both MIT licensed, and pay only for the server. Or sign up for a hosted free tier and pay nothing at all. The hosted route is faster to a working API, and it's the one this walkthrough uses. Draftbase's Hobby plan is free with no card: 1,000 entries, 1 GB of media, two team members, and full API access. Below is the whole path, from schema to a fetch call in your app, plus the point where free stops making sense. For the wider comparison of options, the headless CMS pillar covers the field.

What does "free headless CMS" actually mean?

Two very different things, and the confusion costs people a weekend.

Free software means the code is open source and you can run it. Strapi and Payload are both MIT licensed, so commercial use is fine and there's no seat count to watch. You still need a server, a database, and someone to patch them.

Free tier means a vendor runs it for you and caps what you can store. No server, no database, no on-call. The cap is the price.

Neither is better. They fail in different places, which is the next section.

Self-hosted or a hosted free tier?

Answer this by asking who's going to restart the container at 2am. If nobody on your team wants that job, take the hosted tier.

Self-hosting is free software, not free hosting

A $6/month VPS runs a small Strapi instance. Add Postgres, backups, TLS renewal, and a plan for upgrades. None of those is hard. All of them are yours.

The real cost is the hour you spend the first time an upgrade breaks a plugin. That's not a criticism of Strapi. It's what running software means.

Check the license before you commit

This is where the advice online has gone stale. Directus is widely described as free to self-host under a Business Source License, with a $5 million revenue and 50-employee threshold. That's out of date.

Directus now ships under MSCL-1.0-GPL, and the terms work differently. The license grants use for a "Permitted Purpose" and blocks "Competing Use," which it defines as making the software available to parties competing with the licensor's commercial offerings. There are no revenue or headcount numbers in the text at all, per Directus's own license page.

So if you picked Directus because a listicle told you the $5M rule applied to you, re-read the license. The question changed from "how big are we" to "what are we shipping."

Hosted free tiers trade limits for zero ops

You get a working API in minutes and nothing to maintain. What you don't get is unlimited anything.

Draftbase's Hobby plan caps at 1,000 entries, 1,000 media files, 1 GB of storage, 1 environment, and 2 team members. Those are the exact numbers, and they're on the pricing page rather than behind a sales call. A personal blog or a docs site fits inside them comfortably. A product catalog probably doesn't.

Defining your first template

Content structure comes before content. In Draftbase a schema is a template: a named set of typed fields.

For a blog you'd want something like this:

FieldTypeRequired
titletextyes
slugtextyes
contentrichTextyes
publishedAtdateno
coverImagemediano

The richText field stores plain MDX as a string. Not a JSON node tree, which matters later when you render it: you get Markdown with GFM tables and your own components, rather than a renderer function per node type.

Define the whole field list in one go. Creating a bare template and patching fields onto it afterwards works, but it's twice the calls for the same result.

How do you fetch content once it's set up?

One GET request with a bearer token. Create a delivery API key in the dashboard, put it in .env, and read entries by template.

const res = await fetch(
	'https://api.draftbase.co/delivery/entries?templateId=blogPost&limit=20',
	{
		headers: { Authorization: `Bearer ${process.env.DRAFTBASE_KEY}` },
		next: { tags: ['blogPost'] },
	},
);
const { items } = await res.json();

Two things to know about that endpoint. It's cached and rate-limited per org, so a burst of traffic doesn't turn into a burst of database reads. And it serves published content only. Drafts return nothing, which is correct behavior and surprises people once.

The limit param defaults to 20 and maxes at 100. Past that, page with the after cursor rather than raising the limit.

How do you render the content you just fetched?

The fetch gives you a string. Turning it into React is the step most free-tier guides skip.

Draftbase stores richText as raw MDX. @draftbase/renderer compiles it, and MDXContent is a Server Component, so it renders on the server with no client bundle cost:

import { MDXContent } from '@draftbase/renderer';

export default function Post({ entry }) {
	return <MDXContent source={entry.fields.content} components={{ Callout }} />;
}

Two behaviors worth knowing before you ship this.

It fails soft. Invalid MDX gets logged and rendered as plain text inside an error boundary, rather than throwing and taking the route down. A broken table in one post doesn't 500 your blog.

And import statements don't work in CMS-authored MDX. The compiler runs runtime strings in function-body format, which throws on an import. So the components prop is the only door a component can come through. That's a security boundary as much as an API: an editor can't pull arbitrary code into a page, because you decide what's in the map.

For Pages Router or client-side React, call compileMDX yourself and render the result.

Wiring publishing so it doesn't need a deploy

Add a webhook and your editors stop waiting on CI. Point it at a route in your app, subscribe to entry.published, and clear the matching cache tag when it fires.

export async function POST(req: Request) {
	const { event } = await req.json();
	if (event === 'entry.published') revalidateTag('blogPost');
	return Response.json({ ok: true });
}

Webhooks are on the free plan, so you can build the full publish path before spending anything. We went deeper on this in publishing without a deployment.

The underused angle: free tiers cap different things

Compare free plans on the axis that matches your content, not on the headline.

Some vendors cap entries. Some cap API requests. Some cap seats. A docs site with 300 pages and heavy traffic dies on a request cap and never notices an entry cap. A product catalog with 40,000 SKUs dies on the entry cap while barely touching requests.

So the useful question isn't "which free tier is biggest." It's "which number do I hit first?" Count your entries, estimate your monthly requests, then read the plans in that order. Most people read them in the opposite order and get surprised in month three.

There's a second trap in self-hosted free tiers: cold starts. A free container that sleeps after inactivity will serve your first request in seconds rather than milliseconds. For a blog that's survivable. For an ecommerce page it's revenue.

Free vs paid: when should you upgrade?

Four triggers, and they're all countable rather than vibes.

You pass the entry cap. On Hobby that's 1,000 entries, and it's a hard stop rather than an overage bill.

You need a third teammate. Hobby includes two members. The Startup plan at $49/mo takes it to ten, along with 5,000 entries and a second environment.

You need staging. One environment means you're editing production content in production. That's fine for a personal site and not fine once a client is reading it.

Or your self-hosted instance starts costing real hours. Add up the upgrade afternoons and the backup checks, price them at your rate, and compare that to $49. Often the hosted plan is the cheaper line.

Where to start

Pick hosted if you want an API today, self-hosted if you want control and have somewhere to run it. Both get you to a working content API without a purchase order.

On React or Next.js, writing in MDX? Draftbase is the shortest path. Templates with typed fields, MDX stored as plain strings, REST and GraphQL delivery, and webhooks. The typed SDK ships codegen, so your fields land in TypeScript. Hobby is free forever with no card. When 1,000 entries stops being enough, Startup is $49/mo. Start with free and open-source options if you'd rather compare first.

How to

  1. 1
    Decide between self-hosted and a hosted free tier

    Self-hosting Strapi or Payload is free software but not free hosting, so budget a server, a database, and upgrade time. A hosted free tier gives you a working API in minutes with caps instead of ops. Check the license before committing: Directus now ships under MSCL-1.0-GPL, not the Business Source License terms most guides still quote.

  2. 2
    Create your account and org

    Sign up for the free plan and create an organization. On Draftbase's Hobby plan you get 1,000 entries, 1,000 media files, 1 GB of storage, 1 environment, and 2 team members, with no card required.

  3. 3
    Define a template with every field at once

    A template is your schema: a named set of typed fields such as title (text), slug (text), content (richText), publishedAt (date), and coverImage (media). Define the full field list in one step rather than creating a bare template and patching fields onto it.

  4. 4
    Create your first entry

    Write an entry against the template. richText fields hold plain MDX as a string, so you can use GFM tables and your own registered components without a JSON node tree.

  5. 5
    Generate a delivery API key

    Create a delivery-scoped API key in the dashboard and store it in .env as DRAFTBASE_KEY. Delivery keys read published content only, so they are safe to use from a server-rendered route.

  6. 6
    Fetch entries from your app

    Call GET https://api.draftbase.co/delivery/entries?templateId=blogPost with an Authorization: Bearer header. The limit param defaults to 20 and maxes at 100; page past that with the after cursor.

  7. 7
    Render the MDX

    Pass the richText string to MDXContent from @draftbase/renderer with a components map. It is a Server Component and fails soft, rendering invalid MDX as plain text instead of throwing. Import statements do not work in CMS-authored MDX, so the components map is the only way a component reaches the page.

  8. 8
    Wire a webhook so publishing skips the build

    Add a webhook pointing at a route in your app and subscribe to entry.published. In that route call revalidateTag for the affected content so a publish clears the cache instead of triggering a deploy. Webhooks are included on the free plan.

Ship content that's built to be found

Draftbase generates schema, structured data, and a fast MDX editor for every post.

Frequently asked questions

Can you set up a headless CMS for free?

Yes, two ways. Self-host an MIT-licensed one like Strapi or Payload and pay only for the server, or use a hosted free tier such as Draftbase's Hobby plan, which includes 1,000 entries, 1 GB of media, 2 members, and full API access with no card.

Is self-hosting a headless CMS actually free?

The software is free, the hosting is not. A small instance needs a VPS, a database, TLS renewal, backups, and someone to handle upgrades. Price those hours before assuming self-hosting is the cheaper option.

Which free CMS plan limit will I hit first?

Depends on your content shape. A docs site with heavy traffic hits an API request cap first. A product catalog hits the entry cap first. Count your entries and estimate monthly requests, then compare plans on that number rather than the headline.

Is Directus still free to self-host under $5 million in revenue?

No, that guidance is out of date. Directus now ships under MSCL-1.0-GPL, which grants use for a Permitted Purpose and blocks Competing Use. The license text contains no revenue or employee thresholds.

Working with this hands-on? Draftbase also has a free supabase rls checker.

Related reading

Go deeper on Headless CMS