Originally published on nomacms.com. I work on NomaCMS.
Search for react cms and most lists talk about Sanity Studio, Storyblok, or Payload living inside your repo. That helps when the hard problem is who edits content. It does not always help when the hard problem is who logs in to your app.
A React marketing site might only need published pages and assets. A membership app, customer dashboard, or logged-in community needs structured content and signup, login, and sessions for your users. This post is for that second case: what "React CMS" usually means, how to fetch content from Vite, CRA, or React Router, and when a separate auth vendor still makes sense.
What people mean by "React CMS"
The phrase shows up in three different conversations:
| Meaning | What you are really picking | Examples people mention |
|---|---|---|
| Editor experience | A dashboard marketers use to build pages | Storyblok, Sanity Studio, Prismic |
| Developer experience | Schema and admin live in your JavaScript repo | Payload, custom React admin panels |
| Content delivery | Any headless API your React app consumes | Sanity, Contentful, Strapi, NomaCMS |
React is a frontend. It does not require a CMS that ships a React-specific plugin. What it needs is a stable HTTP API, predictable JSON, and (if you have members) a clear auth story.
Most 2026 roundups score the first two rows: visual editing, TypeScript schema, real-time collaboration. That is fair for content-heavy marketing sites. If your app also has a members area, add a fourth checklist item: where end-user sessions live.
The auth question most roundups skip
Headless CMS products handle editor login. That is how your team opens the dashboard and changes entries.
Your React app often needs a different kind of login: project users (customers, members, subscribers) who sign up inside the product you ship.
Those are not the same credential:
| CMS editor access | End-user (project) auth | |
|---|---|---|
| Who | You, your team, client editors | People using your React app |
| Typical credential | Dashboard login or personal API key | Session access + refresh tokens |
| Purpose | Manage collections, entries, assets | Signup, login, profile, logout |
Many teams pick a strong headless CMS for content, then add Clerk, Auth0, Supabase Auth, or Firebase Auth for users. That works. It also means two products to configure, two docs to maintain, and glue code in your React app or API layer every time a token expires.
If logged-in users are part of the product from day one, ask upfront: is end-user auth bundled with the CMS, or always a second vendor?
How React apps usually talk to a headless CMS
You do not need a React-only SDK. You need JSON over HTTPS.
Option 1: REST from your backend (safest default)
Your React app calls your API. Your server calls the CMS with a personal access token that never ships in the browser bundle.
Every Content API request to NomaCMS uses:
https://app.nomacms.com/api
project-id: <your-project-uuid>
Authorization: Bearer <personal-access-token>
Accept: application/json
Create the token in User Settings → API Keys while the correct workspace is selected. Keep it on the server or in Edge functions, not in VITE_* env vars that end up in client builds.
Option 2: @nomacms/js-sdk in React
The official SDK wraps the same Content API with typed methods and consistent errors:
npm install @nomacms/js-sdk
For end-user auth in the browser, configure token storage. Do not put a full CMS API key in the client bundle:
import { createClient } from "@nomacms/js-sdk"
const noma = createClient({
projectId: import.meta.env.VITE_NOMA_PROJECT_ID!,
projectUserAuth: {
autoRefresh: true,
tokenStorage: {
getAccessToken: () => localStorage.getItem("noma_user_access") ?? undefined,
setAccessToken: (token) =>
token
? localStorage.setItem("noma_user_access", token)
: localStorage.removeItem("noma_user_access"),
getRefreshToken: () => localStorage.getItem("noma_user_refresh") ?? undefined,
setRefreshToken: (token) =>
token
? localStorage.setItem("noma_user_refresh", token)
: localStorage.removeItem("noma_user_refresh"),
clear: () => {
localStorage.removeItem("noma_user_access")
localStorage.removeItem("noma_user_refresh")
},
},
},
})
await noma.signInWithPassword({
email: "user@example.com",
password: "their-password",
})
const user = await noma.me()
Auth methods (signUp, signInWithPassword, signInWithSocial, refreshSession, signOut, and others) use the same client. Social login accepts a provider id_token (Google today).
Important: A project user access token identifies a signed-in app user. It does not replace a personal access token for Content API routes. content.list() and other CMS methods always use your personal access token (apiKey), not the session token from login. Fetch published posts from a server route:
// Server only
const noma = createClient({
projectId: process.env.NOMA_PROJECT_ID!,
apiKey: process.env.NOMA_API_KEY!,
})
const posts = await noma.content.list("posts", {
state: "published",
locale: "en",
paginate: 20,
})
Docs: Project Auth overview · JS SDK setup
Option 3: REST only (no SDK)
React Native and some minimal SPAs skip npm packages and call https://app.nomacms.com/api/auth/login for sessions and your own backend route such as GET /api/posts for published content. Same credential rules: server-side keys for content, user tokens for session routes. The mobile apps guide walks through that pattern.
When content and sessions in one platform helps
Consider a hosted headless CMS with project auth built in when:
- Your React app needs members, not just readers (profiles, saved items, gated pages)
- You want one API base (
https://app.nomacms.com/api) for content and/auth/*routes - You are a solo dev or small team and do not want to operate a CMS server plus a separate auth stack
- You might ship mobile later and want the same REST contract from iOS or Android
NomaCMS fits that shape: collections and fields in the dashboard, delivery over REST and @nomacms/js-sdk, and project auth for signup, login, refresh, logout, profile, sessions, email verification, and user-owned API keys. Every plan includes the REST API, SDK, MCP server, and project auth. Basic starts at $15/mo with a 7-day free trial.
That is not the only valid architecture. It is the path with the least vendor stitching when auth is a product requirement, not a future phase.
When Sanity, Payload, or Storyblok is the better buy
You may still prefer another CMS when:
Sanity or Storyblok: marketers need visual page building and real-time preview more than bundled end-user auth. Plan for a separate auth product if your React app needs login.
Payload (in your repo): your team wants the CMS schema inside the Next.js or React monorepo with TypeScript types everywhere. Payload's auth story is primarily CMS admin access, not a hosted multi-tenant end-user auth layer for your customers. Many teams still add Clerk or similar for app users.
Strapi or Directus (self-hosted): data residency, custom plugins, or zero SaaS dependency is non-negotiable and you have someone to patch servers. Budget time for auth integration unless you build it yourself.
None of these are wrong answers. They optimize for different primary jobs.
A simple decision checklist for React teams
Before you commit:
- Who edits? Developers only, or marketers and clients too?
- Who logs in? Nobody, CMS editors only, or end users in your React app?
- Where do secrets live? API keys on a server you control, not in the client bundle?
- How many apps? One SPA today, or web + mobile sharing the same user accounts?
If only (1) matters, a visual or in-repo CMS from any major roundup is enough. If (2) is yes on launch week, shortlist platforms that ship project auth or accept the cost of a second auth vendor on day one.
Questions welcome in the comments.
Top comments (0)