TinaCMS 3.7 in local mode: an 8.7 MB admin, a free GraphQL backend, and one fork in the road

TinaCMS is the open-source headless CMS that compiles into your Next.js / Hugo / Astro site as a React app at /admin. The pitch — "Git-based, no database, Apache 2.0, edit markdown in a real WYSIWYG" — is mostly true. What the homepage doesn't say loudly: the default starter assumes you'll sign up for TinaCloud, and the local-only path needs an extra GraphQL daemon kept alive in dev. Here's what we actually ran, with the bundle numbers and the real /admin screenshot.

Demo: SimpleReview on a TinaCMS issue

localhost:3000/admin/index.html#/collections/post
SimpleReview
TinaCMS · my-tina-nextjs-starter
Blog PostsPagesAuthors
⚠ You are in local mode
Learning about Markdownposts/learning-about-markdown.mdx
What is TinaCMSposts/what-is-tina.mdx
The Wonderful World of Atomposts/atom.mdx
⚠ tinacms build --local crashes Next.js
Error: connect ECONNREFUSED 127.0.0.1:4001
at TCPConnectWrap.afterConnect [as oncomplete]
# GraphQL backend killed before page-data collection
✓ Bundle built · admin/ 12 MiB · 104 chunks
# Run prod build with backend kept alive:
$ tinacms dev --noWatch &
$ next build && next export
$ ls public/admin/ | wc -l
# 104 chunks · main 8.7 MB / 2.5 MB gzip
Comment×
build kills GraphQL backend first|
Fix it ✓ Done
waiting for selection…
Detected
ErrnoECONNREFUSED
Port4001
Fix plan
Keep tinacms dev --noWatch running while next build collects page data.
Result
12 MiB admin bundle · 104 chunks · 2.5 MB gzip.
✓ Fix ready
fix(build): keep tina backend alive
1 line · package.json scripts
Click SimpleReview → select ECONNREFUSEDFix it → Tina backend kept alive during next build
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 TinaCMS project or Tina Inc. (the company behind TinaCloud). This is a deployment note from one real local install on one Linux box (kernel 5.15, Node 22.21.1, npm scaffold of [email protected] with the official tina-nextjs-starter template) on the date above. If we got something wrong, open a GitHub issue and we'll fix the page.

The 30-minute local install

The docs lead with npx create-tina-app@latest. The wizard rejects the canonical tina-cloud-starter from the docs and tells you the real list:

$ npx --yes create-tina-app@latest my-tina --template tina-cloud-starter --pkg-manager npm
🦙 TinaCMS
Create Tina App v2.1.4
✖ The provided template 'tina-cloud-starter' is invalid.
  Please provide one of the following:
  tina-nextjs-starter, tina-docs, tina-astro-starter,
  tina-hugo-starter, tina-remix-starter, tinasaurus, basic

The README's recommended starter doesn't exist under that name any more. The replacement is tina-nextjs-starter:

$ npx --yes create-tina-app@latest my-tina \
    --template tina-nextjs-starter --pkg-manager npm
✔ Downloading files from repo tinacms/tina-nextjs-starter
✔ Installing packages.
✔ Initializing git repository.
✔ Created my-tina

Install size: 1.5 GB of node_modules. The interesting folder is tina/:

$ ls my-tina/tina
collection/    config.tsx     fields/    queries/
__generated__/                # GraphQL schema + types Tina writes itself
tina-lock.json                # 132 KB schema snapshot

Configuration is one file, tina/config.tsx. The default ships fully cloud-coupled:

const config = defineConfig({
  clientId: process.env.NEXT_PUBLIC_TINA_CLIENT_ID!,    // TinaCloud project id
  branch:   process.env.NEXT_PUBLIC_TINA_BRANCH! || ...,
  token:    process.env.TINA_TOKEN!,                    // TinaCloud read token
  media: { tina: { publicFolder: "public", mediaRoot: "uploads" } },
  build:  { publicFolder: "public", outputFolder: "admin", basePath: "" },
  schema: { collections: [Page, Post, Author, Tag, Global] },
});

Three env vars (clientId, branch, token) point at TinaCloud. There's no comment explaining that you can leave them empty and run locally — you just have to know tinacms dev ignores them and substitutes a local datalayer.

Booting it

The starter's npm run dev wraps two daemons in one command:

"scripts": {
  "dev": "tinacms dev -c \"next dev --turbopack\"",
  ...
}

tinacms dev starts a local GraphQL server (Vite+Express) on 4001, indexes the content/ tree, generates tina/__generated__/types.ts, then spawns the next dev process you supplied with -c:

$ NEXT_PUBLIC_TINA_CLIENT_ID="" TINA_TOKEN="" \
    npx tinacms dev -c "next dev --turbopack -p 18094"

🦙 TinaCMS Dev Server is initializing...
   Indexing local files ⠋

✅ 🦙 TinaCMS Dev Server is active:
   CMS:                http://localhost:18094/admin/index.html
   API playground:     http://localhost:18094/admin/index.html#/graphql
   API url:            http://localhost:4001/graphql

Running web application with command: next dev --turbopack -p 18094
   ▲ Next.js 15.3.8 (Turbopack)
   - Local:        http://localhost:18094
   ✓ Ready in 1271ms

So two processes — Tina on 4001, Next.js on 18094. The admin HTML is served by Next.js; its JS module URLs include hard-coded http://localhost:4001/@vite/client — fine on a single dev box, but bites the moment you open the admin from another machine on your LAN. We hit the "Failed loading TinaCMS assets" error page once because of exactly this, until we switched the screenshot URL back to localhost on the same host.

What's in content/

The starter ships 16 demo files across posts/ (six .mdx, one nested in posts/june/ to demo folder collections), authors/, pages/, tags/, and a global/index.json. The schema doesn't restrict where files live within a collection's root path — a meaningful improvement over Decap's flat folders. The Next.js front:

Tina Starter homepage with a llama logo, top button reading 'Support for live editing and editorial workflow', large headline 'Welcome to the TinaCloud Starter', subheading 'Build 10x Faster with TinaCMS', and Get Started / Read Blog buttons
The default tina-nextjs-starter on first paint at /. Llama logo, "TinaCloud Starter" headline (the template's name still says TinaCloud even though local mode works), and a top banner advertising "live editing and editorial workflow" — that link goes to TinaCloud's docs, not a self-host guide.

The /admin route, in real life

Open http://localhost:18094/admin/index.html and you get a modal that's trying to be helpful and is actually the most important UX detail in the whole product:

┌─ Enter into edit mode ───────────────────────────────┐
│  When you save, changes will be saved to the local   │
│  filesystem.                                         │
│                                                      │
│         [ Enter Edit Mode ]                          │
└──────────────────────────────────────────────────────┘

This modal has to be dismissed every time you open /admin in a new tab — no "remember my choice". If you came here expecting a TinaCloud login screen, the modal is the only signal that local mode is what you're in. Click through and you land in the collection list:

TinaCMS admin showing Blog Posts collection with 6 .mdx entries listed — Learning about Markdown, Mermaid, TinaCMS, TinaCloud, components, and 'Learning to blog' — each with full filename path under content/posts/. Top of page shows a yellow 'You are in local mode' badge, an info banner reads 'You have not configured search. Read the docs', and Add Folder / Add File buttons sit on the right.
Real Blog Posts collection list under tinacms 3.7.5 on local mode. The You are in local mode banner top-left is permanent — click it and a tooltip explains saves go to disk, not to a Git host. The Read the docs info banner is the search story: full-text search inside content is a TinaCloud feature, not bundled in the local datalayer. The .mdx column on the right is the actual file extension being read.

Click into any row and the editor opens with a real two-pane layout — form fields on the left, live preview of the rendered MDX on the right:

TinaCMS visual editor with left sidebar showing Title input set to 'Learning about Markdown', Hero Image with a llama drawing, Excerpt rich-text field with markdown content, Reset and Save buttons greyed out, and right pane showing the live-rendered article with hero image, byline, and 'What is Markdown?' heading.
The actual editor at /admin/index.html#/collections/post/learning-about-markdown. Sidebar fields are generated from tina/collection/post.tsx. Right pane is the same Next.js page (app/posts/[...urlSegments]/page.tsx) with Tina's useTina hook subscribed to live form changes — type in the Title field and the heading on the right updates with no reload. Save / Reset are disabled until something dirty actually exists.

This is the moment Tina earns its existence. Decap CMS gives you a form on the left and nothing on the right. Strapi has a separate "preview" tab. Tina puts your real production page on the right and rebinds it to your draft data — the only setup is a useTina() hook in your route. For teams where editors are also the people writing the design, that round-trip is shorter than every other Git-based CMS we've run.

The fork in the road: local datalayer vs TinaCloud

Tina's not a single product. It's two things sharing a name:

  • The OSS package (tinacms + @tinacms/cli, Apache 2.0, on GitHub at tinacms/tinacms). This is what runs tinacms dev, the local datalayer, and the /admin React app. You can ship a static Next.js export with the admin baked in and never touch a paid service.
  • TinaCloud (proprietary, hosted by Tina Inc.). This is the production GraphQL backend that signs in editors via OAuth, serves the schema, indexes content, runs full-text search, and wraps Git commits behind a per-user ACL. Free tier, paid tiers above.

The OSS readme treats these as a continuum — "use TinaCloud or self-host the backend". In practice the friction is not symmetric:

FeatureLocal datalayer (tinacms dev)TinaCloud
Edit content in browseryesyes
Save to disk on dev boxyesno — commits to Git
Multi-user editingno authOAuth + roles
Full-text searchmissingindexed
Editorial workflow / draftsfilesystem onlyPR-based
Costfree, Apache 2.0free tier → paid

You can deploy the static Tina admin (from tinacms build --local) against a GraphQL backend you run yourself — the daemon code is in packages/datalayer. But it's not packaged as "drop in this Docker container" the way Strapi or Directus is. Most articles titled "self-hosting TinaCMS" stop at "point at TinaCloud, it's free for one user". That's not self-hosting; that's a hosted backend with a free tier.

What we observed

"Local mode" is not the same thing as "self-hosted production". Local mode is what runs on a dev laptop with the GraphQL daemon as a sibling process, writing edits straight to the working tree. To go to production without TinaCloud you need to operate the GraphQL backend somewhere reachable by your editors' browsers, plus solve auth yourself, plus pick how the index gets rebuilt on commits.

This isn't documented as a fork in the road. It's documented as "step 1: scaffold app, step 2: deploy to Vercel, step 3: connect to TinaCloud". Anyone planning to skip step 3 has a non-trivial amount of plumbing left.

What the bundle actually weighs

We ran npm run build-local (which is tinacms build --local --skip-indexing --skip-cloud-checks) to compile the production admin bundle into public/admin. Then we measured the output:

MetricValueNotes
Total public/admin/12 MiBOne index.html + 104 JS chunks + 1 CSS file + svgs
Main entry JS (uncompressed)8.30 MiBassets/index-7719c402.js — React + Tina runtime + form lib + GraphQL client
Main entry JS (gzip)2.40 MiBWhat a real browser pulls on cold load
All JS, total10.99 MiB / 3.21 MiBUncompressed / gzip across 104 chunks
Main CSS510 KB / 287 KBTina admin stylesheet (Tailwind + custom)
node_modules at install time1.5 GiBIncludes Next 15, React 18, Mermaid, react-icons, etc.
Tina lock file132 KBSnapshot of generated schema, committed to git
Cold first paint of admin~3.5 sLocal network. ~2 s of that is the explicit "if no #root children after 2s, show error" timeout the loader uses

For context: the equivalent Decap CMS bundle we measured the same day was 5.66 MiB uncompressed / 1.65 MiB on the wire. Tina ships roughly twice the JS. The trade is real: you get side-by-side live preview, custom field components in React, and a Mermaid renderer baked in. Mermaid alone is ~1 MiB of that delta — the diagram chunks lazy-load, but the main bundle still imports the parser shell. If you don't render Mermaid you can drop it (remove the import from tina/config.tsx and the rich-text plugin) and shave ~800 KB.

Quiet warnings worth knowing about

  • Two daemons, two ports. tinacms dev binds 4001 (GraphQL) and 9000 (datalayer / search index). If either is busy you get a confusing "Tina Dev server is already in use. Datalayer server is busy on port 9000". Killing only next dev isn't enough — the Tina daemon survives independently. Run pkill -f "tinacms dev" when in doubt.
  • Hard-coded localhost:4001 in dev. The dev admin HTML inlines a Vite preamble that loads http://localhost:4001/@vite/client. Open the admin from another machine on your LAN (e.g. from a phone for mobile QA), the vite client fails to connect, the React app never mounts, and after 2 seconds the static fallback reads "Failed loading TinaCMS assets". The config is fine; the URL is wrong. Not in the docs.
  • Build-local can break Next.js page-data collection. If tinacms build --local shuts down the GraphQL backend before next build tries to fetch from it, the Next build fails with TypeError: fetch failed ECONNREFUSED. The starter's build-local script does this. You're effectively expected to use TinaCloud in production, where the race doesn't happen.
  • The starter still says "TinaCloud" everywhere. The homepage headline is "Welcome to the TinaCloud Starter". The "Get Started" link points at TinaCloud signup. If you didn't read config.tsx you'd not know local mode existed.
  • Search is a TinaCloud feature. The "You have not configured search" banner in the collection list is permanent in local mode. The OSS datalayer doesn't ship a search index; that's a billable feature.
  • 1.5 GB of node_modules. Next 15 and the Tailwind / motion / mermaid / radix stack are not small. CI cache strategy matters more than for a Decap-style "one bundle from CDN" install.

Things we'd change in the README

  1. Lead with the fork. The first decision is local-only vs TinaCloud, not "scaffold an app". A 200-word section near the top with an explicit feature-difference table would save people picking the wrong path twice.
  2. Fix the wizard's starter list. The docs still reference tina-cloud-starter; that template is gone. tina-nextjs-starter is the default now.
  3. Surface "Local mode" before the modal. The "Enter Edit Mode" modal could read: "Local mode — writes to ./content/ on the dev box. For production with auth, use TinaCloud or self-host the backend."
  4. Ship a self-host-the-backend recipe. A Dockerfile + docker-compose snippet for the GraphQL daemon with OAuth wiring would replace a half-dozen forum posts.
  5. Decouple Mermaid. Projects that don't render diagrams in the editor shouldn't ship the parser. A defineConfig opt-out would shrink the bundle by ~1 MiB.

Where TinaCMS fits in 2026

Tina is the right pick when you want a real visual editor on top of Git-tracked markdown, your editors will write in a CMS form rather than directly in .md, and either (a) you'll pay for TinaCloud's hosted backend, or (b) you have engineering capacity to run the GraphQL daemon yourself. Local-only is genuinely usable for solo blogs, docs sites, and small marketing sites where one developer is the editor.

It's the wrong pick when "no backend service in production" is a hard requirement — Decap is straighter for that case, even with its test-repo footgun. It's also wrong for teams of ten with media-heavy uploads and role-based ACLs who haven't budgeted for TinaCloud — Strapi, Directus, or Payload will be cheaper-and-happier. Local mode is powerful enough to fool you into thinking you've solved production, light on warnings about what comes next.

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: Decap CMS 3.12 in 30 lines (closest sibling — also Git-based, also MIT, simpler architecture), 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 — including a Tina collection editor that won't save — into a draft code-fix PR you can ship without leaving the page.