TinaCMS is a Git-backed headless CMS: instead of storing content in a database, it edits the Markdown and JSON files that already live in your repository, then commits the changes back to Git. For Jamstack sites this is appealing — your content is version-controlled, reviewable in pull requests, and there's no separate database to operate. But "Git-backed" hides some architecture you need to understand before deploying. This guide covers it.
How TinaCMS actually works
Tina has three moving parts:
- 1The editor UI — a React app, usually mounted at
/adminon your site, that gives content editors a visual form-based interface. - 2The content — Markdown/MDX/JSON files in your repo, described by a schema in
tina/config.ts. - 3The data layer / GraphQL API — Tina indexes your content into a GraphQL API. In production this is backed by Tina Cloud (the managed option) or a self-hosted data layer (Tina's self-hosted backend handler plus a database like MongoDB and a Git-token-authenticated backend).
The key decision is Tina Cloud vs self-hosted backend. Tina Cloud handles auth, the data layer, and Git commits for you. Self-hosting means you run the backend handler yourself and bring your own auth and database. Most teams start with Tina Cloud; self-hosting is for those who need full control.
Option A: Tina Cloud (managed backend)
This is the simplest production path. You sign up for Tina Cloud, connect your GitHub repo, and get a client ID and read-only token. Your site stays static; the editor talks to Tina Cloud, which commits to your repo.
Set these env vars in your build/runtime environment:
NEXT_PUBLIC_TINA_CLIENT_ID=your_client_id
TINA_TOKEN=your_read_only_token
TINA_SEARCH_TOKEN=optional_search_tokenBuild with Tina's wrapper so the admin and GraphQL types are generated:
{
"scripts": {
"dev": "tinacms dev -c \"next dev\"",
"build": "tinacms build && next build"
}
}The tinacms build step generates the /admin/index.html editor and the typed GraphQL client. Deploy the resulting site as you normally would.
Option B: Self-hosted backend
If you self-host, you run Tina's backend handler (an API route) that handles auth and Git operations, backed by a database. The reference setup uses:
- A backend API route (
/api/tina/[...routes]) using@tinacms/datalayer - A database adapter (MongoDB is the documented default) for the content index
- An auth provider (Tina's
AuthJsBackendAuthProvideror your own) - A GitHub personal access token for commits
The backend needs a database. TinaCMS documents MongoDB for the self-hosted data layer. On PandaStack you can provision a managed MongoDB and inject its connection string as an env var, so the self-hosted backend has a durable index without you operating a database. Set:
GITHUB_PERSONAL_ACCESS_TOKEN=...
GITHUB_OWNER=your-org
GITHUB_REPO=your-repo
GITHUB_BRANCH=main
MONGODB_URI=... # from your managed MongoDB
NEXTAUTH_SECRET=...Because the self-hosted backend is a Node server (API routes), this variant is a container deployment, not a pure static site. Your Next.js app with the Tina backend route deploys as a container; on PandaStack that's a Dockerfile or buildpack build via rootless BuildKit, with the managed MongoDB auto-wired.
Deploying the editor and site together
The nice property of Tina is that the editor (/admin) and your site ship together — the editor is just static assets generated at build time. So:
- Static site + Tina Cloud: deploy as a static site. The
/adminroute is static; the data layer is Tina Cloud. On PandaStack this is a free static deployment with automatic SSL. - Self-hosted backend: deploy as a container (the backend route needs a server runtime), with a managed MongoDB attached.
For the static-site path, configure:
- Build command:
tinacms build && next build(or your framework's equivalent — Tina supports Hugo, Astro, etc., via its content layer) - Output: your framework's build output (e.g.
.nextfor SSR,outfor static export) - Env vars:
NEXT_PUBLIC_TINA_CLIENT_IDat build time,TINA_TOKENavailable to the build
Editorial workflow and branching
Tina supports an editorial workflow where edits go to a branch and become a pull request rather than committing straight to main. This pairs well with preview deployments: each branch/PR can get its own preview build so editors see changes before merge. If your host supports per-branch deploys, wire that up — it turns content editing into the same reviewable flow as code.
Common pitfalls
- Forgetting
tinacms build. Running only your framework's build skips generating/adminand the typed client. The editor 404s. - Token scope. The GitHub token (self-hosted) or Tina Cloud token needs the right repo permissions to commit. Read-only tokens can read but not save edits.
- CORS / domain mismatch (Tina Cloud). Add your deployed domain to the allowed sites in the Tina Cloud project settings, or auth will fail.
- Large media. Tina can store media in the repo or in an external store (e.g. Tina's media or a cloud bucket). Committing large binaries to Git bloats the repo fast — use an external media store for image-heavy sites.
Which option to choose
| Need | Choose |
|---|---|
| Simple, fast, no DB to run | Tina Cloud + static site |
| Full data residency / no third-party | Self-hosted backend + managed MongoDB (container) |
| Editorial review via PRs | Either, with branch-based workflow |
References
- TinaCMS docs: https://tina.io/docs/
- Self-hosted TinaCMS guide: https://tina.io/docs/self-hosted/overview/
- Tina GraphQL / data layer: https://tina.io/docs/graphql/overview/
- Tina editorial workflow: https://tina.io/docs/drafts/editorial-workflow/
- MongoDB documentation: https://www.mongodb.com/docs/
---
Going self-hosted with TinaCMS? PandaStack gives you a container deployment plus a managed MongoDB with backups, auto-wired via env vars — and a free static tier if you stay on Tina Cloud. Start at https://dashboard.pandastack.io.