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
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-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 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:
/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 attinacms/tinacms). This is what runstinacms dev, the local datalayer, and the/adminReact 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:
| Feature | Local datalayer (tinacms dev) | TinaCloud |
|---|---|---|
| Edit content in browser | yes | yes |
| Save to disk on dev box | yes | no — commits to Git |
| Multi-user editing | no auth | OAuth + roles |
| Full-text search | missing | indexed |
| Editorial workflow / drafts | filesystem only | PR-based |
| Cost | free, Apache 2.0 | free 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.
"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:
| Metric | Value | Notes |
|---|---|---|
Total public/admin/ | 12 MiB | One index.html + 104 JS chunks + 1 CSS file + svgs |
| Main entry JS (uncompressed) | 8.30 MiB | assets/index-7719c402.js — React + Tina runtime + form lib + GraphQL client |
| Main entry JS (gzip) | 2.40 MiB | What a real browser pulls on cold load |
| All JS, total | 10.99 MiB / 3.21 MiB | Uncompressed / gzip across 104 chunks |
| Main CSS | 510 KB / 287 KB | Tina admin stylesheet (Tailwind + custom) |
node_modules at install time | 1.5 GiB | Includes Next 15, React 18, Mermaid, react-icons, etc. |
| Tina lock file | 132 KB | Snapshot of generated schema, committed to git |
| Cold first paint of admin | ~3.5 s | Local 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 devbinds4001(GraphQL) and9000(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 onlynext devisn't enough — the Tina daemon survives independently. Runpkill -f "tinacms dev"when in doubt. - Hard-coded
localhost:4001in dev. The dev admin HTML inlines a Vite preamble that loadshttp://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 --localshuts down the GraphQL backend beforenext buildtries to fetch from it, the Next build fails withTypeError: fetch failed ECONNREFUSED. The starter'sbuild-localscript 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.tsxyou'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
- 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.
- Fix the wizard's starter list. The docs still reference
tina-cloud-starter; that template is gone.tina-nextjs-starteris the default now. - 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." - 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.
- Decouple Mermaid. Projects that don't render diagrams in the editor shouldn't ship the parser. A
defineConfigopt-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.