Decap CMS 3.12 in 30 lines: what test-repo actually saves, and what it doesn't

Decap CMS (the project formerly known as Netlify CMS, forked in February 2023 after Netlify stopped maintaining it) is unusual: it's a single 5.6 MB JavaScript bundle that turns any folder with an admin/index.html into a content editor. Setup looks trivial. The friction is hidden one layer down — most people's first run uses the test-repo backend, and "Save" doesn't write anywhere they expect. Here's what we ran, what we screenshotted, and the localStorage keys that explain it.

Demo: SimpleReview on a Decap CMS issue

localhost:18093/admin/#/collections/blog
SimpleReview
Decap CMS · Editorial Workflow
BlogPagesSettings
My first post (draft) UNSAVED
About the team CHANGES SAVED
⚠ Backend = test-repo · changes never reach git
localStorage[decap-cms-user] = {"backendName":"test-repo"}
"CHANGES SAVED" indicator is a lie when backend is test-repo.
Browser refresh → all drafts gone.
✓ Backend swapped · git-gateway active
# admin/config.yml
backend:
  name: git-gateway
  branch: main
$ curl https://api.github.com/repos/me/site/commits/main
# → "message": "Create blog/2026-05-07-my-first-post.md"
Comment×
test-repo only writes to localStorage|
Fix it ✓ Done
waiting for selection…
Detected
Backendtest-repo
StoragelocalStorage
Fix plan
Replace test-repo with git-gateway or github in admin/config.yml
Result
Drafts commit to git. Survive refresh + cross-browser.
✓ Fix ready
fix(decap): swap test-repo for git-gateway
1 file · admin/config.yml
Click SimpleReview → select "CHANGES SAVED" indicator → Fix it → real git backend wired in
Honest about what this is

We're the team behind SimpleReview, a Chrome extension that turns the element you click on a broken admin into a draft code-fix PR. We're not affiliated with the Decap CMS project or Netlify. This is a deployment note from one real local install on one Linux box (kernel 5.15, Docker 24, Node served via python3 -m http.server) on the date above. If we got something wrong, open a GitHub issue and we'll fix the page.

The 30-line static site that boots a CMS

Decap is a React + Redux app shipped as one webpack bundle. There's no server-side install. The "site" is two files in an admin/ folder, plus whatever static host you already have. Ours:

$ tree /tmp/decap-test
/tmp/decap-test
├── admin
│   ├── config.yml
│   └── index.html
└── index.html

The admin/index.html is twelve lines including the doctype:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Content Manager</title>
  </head>
  <body>
    <script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
  </body>
</html>

And admin/config.yml declares the backend and one collection:

backend:
  name: test-repo

media_folder: "static/uploads"
public_folder: "/uploads"

collections:
  - name: "blog"
    label: "Blog"
    folder: "content/blog"
    create: true
    fields:
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Publish Date", name: "date", widget: "datetime" }
      - { label: "Body", name: "body", widget: "markdown" }
  - name: "pages"
    label: "Pages"
    folder: "content/pages"
    create: true
    fields:
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Body", name: "body", widget: "markdown" }

Serve it with anything that hands back static files. We used Python's stdlib:

$ cd /tmp/decap-test && python3 -m http.server 18093
Serving HTTP on 0.0.0.0 port 18093 (http://0.0.0.0:18093/) ...

Open http://localhost:18093/admin/ and the bundle paints a login screen four seconds later:

Decap CMS login screen — pink Decap logo with a single dark Login button
Decap CMS 3.12.2 first paint at /admin/ with the test-repo backend. The pink logo is hard to miss; the friction is also hard to miss — there's no username, no password, no field. Just "Login". This is the whole login screen for test-repo: clicking the button accepts you as a fictional user.

What "test-repo" backend actually means

You'd assume from the name that test-repo writes to a local repo. It doesn't. There is no repo. The Decap docs describe it as "a backend that lives entirely in the browser, useful for offline demos", but in practice that wording underplays it. Click "Login", click New Blog, fill in a title — the editor flashes CHANGES SAVED in the toolbar before you've typed anything:

Decap CMS new entry editor — empty Title, empty Publish Date, Body section with Rich Text/Markdown toggle, top toolbar showing CHANGES SAVED
The new-entry editor on a brand-new entry, before a single keystroke. The green CHANGES SAVED pill is already visible. Decap is autosaving an entry that has no title, no date, no body. The "save" target, however, is not your collection folder.

Open DevTools and look at localStorage:

DevTools-style overlay showing localStorage keys — cms.md-mode, cms.scroll-sync-enabled, decap-cms.entries.viewStyle, decap-cms-user with backendName test-repo, cms.preview-visible
Real localStorage contents on http://localhost:18093. The decap-cms-user key is the entire authentication state for test-repo — a literal JSON blob {"backendName":"test-repo"}. Clear the storage and you're "logged out" instantly. The collection drafts you create live in the same storage; nothing is written to content/blog/ on disk.
What we observed

With backend.name: test-repo, Decap stores entries, drafts, and user identity in browser localStorage. CHANGES SAVED means "saved to your tab's storage". Closing the tab keeps the data; clearing site data destroys it. There is no commit, no API call, no file write. The collection folder: content/blog in the config is the path Decap would use against a real backend; with test-repo it's only a label.

This is not a bug — it's the documented behaviour of test-repo. But it's a pothole if you're new: people demo Decap to a client, the client types a draft in the demo, the team migrates the config to backend.name: github, and the draft is gone.

The dashboard, once you're past the login

Click "Login", and the dashboard renders. Two collections from the YAML on the left, "Test Backend ↗" pinned to the top right as a permanent reminder of which backend is in use:

Decap CMS admin dashboard — left sidebar lists Blog and Pages collections, main panel shows Blog with No Entries and a New Blog button, top right shows Test Backend label and Quick add
Real admin shell. Two collections from config.yml, an empty Blog list, and the Test Backend ↗ tag on the right. The "↗" arrow links to the Decap docs page about test-repo — we like that detail; most apps don't tell you what mode you're in this loudly.

Visually, this is closer to a 2018 React admin than a 2026 one. The widget set (Markdown body, datetime, image, list, relation, file) is solid; the polish is uneven. The Markdown widget has a Rich Text/Markdown toggle that genuinely round-trips; the date widget is the unstyled HTML <input type="date"> with a "Now" / "Clear" pair welded on. We'll take it.

Switching to a real backend: GitHub friction

Swap config.yml for the real backend and re-load:

backend:
  name: github
  repo: example-org/example-decap-site
  branch: main

The login screen changes — same Decap logo, but the button is now "Login with GitHub":

Decap CMS login screen with GitHub backend — Decap logo and a dark Login with GitHub button featuring a GitHub octocat icon
Same admin bundle, different backend. The button now points at GitHub's OAuth flow. This is also where setup gets non-trivial.

Click that button without an OAuth provider, and the popup goes nowhere — Decap doesn't ship its own OAuth server, because GitHub doesn't allow client-side OAuth: the token has to come from a confidential client. Your options as of 2026:

  • Netlify Identity / Git Gateway. Easiest, but means standing up a Netlify project even if your hosting isn't on Netlify. The Netlify-Decap split in 2023 didn't break this integration, but it does feel like a vestigial limb on what's now a third-party project.
  • Cloudflare Workers OAuth proxy. A community pattern (the popular one is decap-proxy): a Worker that holds the GitHub client secret and exchanges the code for a token. Two env vars, one Worker. Once running, free-tier traffic costs nothing.
  • Self-host the OAuth server. The Decap repo includes oauth-provider code; deploy it on Render / Fly / your own VPS. More moving parts, but no third-party dependency.
  • Switch to git-gateway. Same Netlify-bundled service that ran for Netlify CMS, still works for Decap. Requires Netlify Identity.

None of these are documented prominently on the Decap site's getting-started flow. The path you actually land on after clicking "Login with GitHub" without setup is a popup that opens the OAuth URL with no client_id, GitHub returns a generic error page, and the popup closes. The Decap admin tab is unchanged. There's no toast, no console error in the parent window. You're left wondering whether to debug your OAuth provider or your config.

What we measured

MetricValueNotes
Bundle (HTTP, gzipped)1.65 MiBWhat the browser actually downloads from unpkg
Bundle (uncompressed)5.66 MiBSize on disk after Content-Encoding decode
Cold load (one fetch)0.5 – 0.6 sEU server to cf-cache HIT on Cloudflare; first paint adds ~1 s of React boot
Login → dashboard (test-repo)~2 sNo network, just Redux store init
Backend file I/O for content0 bytesTest-repo writes only to localStorage
Admin source files needed2admin/index.html + admin/config.yml

For a client-side-only CMS the bundle is fine. 1.65 MiB on the wire is roughly two typical hero images; gzipped JS doesn't compete with image weight on real content sites. The number that matters is the second visit — once the bundle's in HTTP cache, Decap boots in well under a second, which is the experience editors actually have day-to-day.

Quiet warnings worth knowing about

  • The config is fetched, not bundled. Decap reads /admin/config.yml with a fetch() call. Cache-busting matters when you edit the YAML — we hit a 304 once and spent ten minutes wondering why a renamed field wasn't appearing.
  • You can't drop in a new collection without a hard refresh. Even though Decap polls config.yml on focus, runtime config changes don't propagate cleanly. Editors complaining "I don't see the new field" usually need a Cmd-Shift-R.
  • Pin a version in production. The @^3.0.0 selector we used resolves to the latest 3.x at request time (3.12.2 today). For real sites use @3.12.2 exactly — Decap minor releases have introduced editor regressions before. unpkg redirects ^3.0.0 with HTTP 302 → /[email protected]/dist/decap-cms.js, which is fine for hobby sites and exactly what you don't want for client work.
  • Editorial Workflow needs a real backend. The publish_mode: editorial_workflow setting (PR-based draft/review/publish) is the killer feature, but it's a no-op under test-repo. You can't validate the review flow without already having OAuth and a real repo wired up, which is exactly when the bug rate spikes for new installs.
  • Image uploads under test-repo go nowhere. Same story as text drafts: the file lives in browser memory while the tab is open, then it's gone. The Media Library tab will sit at "No assets found" between sessions, which is correct but disorienting for first-time users.

The fix the README could ship today

  1. Rename test-repo in the UI. "Test Backend ↗" is a start — but the dashboard could carry a yellow strip: "Drafts saved here are stored in your browser only and will be lost when site data is cleared. To save to a real repo, configure a Git backend." Anyone who has demoed Decap to a client has been bitten by the absence of this line.
  2. Surface the OAuth failure mode. When the popup closes after a GitHub OAuth bounce-back without setup, the parent admin should toast: "Couldn't reach OAuth provider — see backend.base_url in config." Right now the silence reads as "this is just slow".
  3. Bundle a one-page recipe for Cloudflare Workers OAuth. The community proxy is the de-facto standard now that Netlify's not the only host. A Decap-blessed copy of the Worker code, with the right backend.base_url snippet, would cut the average new-install time to working state by an hour, easily.
  4. Pin the version in create-decap-cms-app output. Templates that scaffold <script src="...^3.0.0..."> bake in a future regression risk.

Where Decap fits in 2026

Decap is the right tool for a specific narrow case: a small site (Hugo, Eleventy, Next-static, Astro) where the editor is one or two technical-ish people, the source of truth is a Git repo, and pull-request-based review of content is genuinely useful. For that case, Decap's combination of zero backend, real Git history per edit, and free hosting on whatever already serves your site is unbeatable.

It's the wrong tool for a content team of ten with media-heavy uploads, role-based permissions, or non-Git editorial workflows. Strapi, Directus, or Payload CMS each handle that better. Ghost handles it better still if all you want is a blog. Where Decap wins is when "the CMS shouldn't add a server" is a hard requirement — that's still a real shape of project, especially for documentation sites and side-project blogs.

The test-repo default is the rough edge worth knowing about before you demo it. Once you've seen the localStorage screenshot above, the friction stops being mysterious and starts being something you can route around — either by skipping the demo and going straight to a Git backend with OAuth, or by warning the client up front that drafts in this view aren't real until the GitHub login screen replaces the test one.

Where this fits

One short, honest write-up per self-hostable CMS / LLM / booking tool we run on a real Linux box. Adjacent articles in the same series: Ghost 5 on Docker SQLite, Open WebUI on Linux Docker, Dify 1.14.0 docker-compose, Cal.com 6.16.1 self-host, PostHog hobby self-host. SimpleReview is the Chrome extension that turns whatever element you click on a broken admin — a misbehaving Decap collection editor included — into a draft code-fix PR you can ship without leaving the page.