BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 10, 2026React RouterBlogAuth

React Router Blog Admin: Protect Loaders, Actions, and APIs

Define editorial permissions and enforce them in React Router server loaders, actions, and the APIs used by your blog editor.

React Router Blog Admin: Protect Loaders, Actions, and APIs

Adding an editor page to a React Router blog creates several entry points: the page loader, the action that saves a form, and any plugin API the page calls. A permission check in one of those places does not automatically protect the others.

This guide focuses on editorial access in React Router v7 Framework Mode. It complements the React Router Blog installation walkthrough: first make the feature run, then make the identity and editorial policy explicit at every server boundary.

Define editorial permissions before wiring the page#

Start with the operations the application actually needs. An ordinary signed-in customer might comment on published posts but should not gain access to the draft list. An editor might create drafts without being allowed to publish or delete them.

OperationExample policy question
Read a published postIs this public content?
List or preview draftsIs this person an editor for this publication?
Update a postDoes this editor own the post or have broader edit access?
Publish or unpublishDoes this identity have publishing authority?
DeleteIs this destructive action separately permitted?

These are application decisions. Do not infer them from a successful authentication check alone. If your application has multiple organizations, resolve the relevant organization and membership on the server as part of the resource policy.

A loader and an action each need a guard#

React Router Framework Mode runs server loaders for data reads and server actions for mutations. Browser navigations and form submissions can call those boundaries without rerunning the particular parent-page check you expected. Its data-loading reference and action reference describe those separate execution paths.

The following sketch makes the boundary visible. The session, permission, and database helpers are application-owned placeholders; implement them against your actual auth provider and data model.

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
export async function loader({ request, params }) {
  const editor = await requireEditor(request);
  const post = await loadPost(params.postId);
  await assertCanEdit(editor, post);
  return { post };
}

export async function action({ request, params }) {
  const editor = await requireEditor(request);
  const post = await loadPost(params.postId);
  await assertCanEdit(editor, post);
  const input = parsePostUpdate(await request.formData());
  return saveAuthorizedPost(editor, post, input);
}

This is policy pseudocode, not a drop-in BTST API example. In a real implementation, handle a missing post, validate input, distinguish editing from publishing, and protect against changes between the policy check and the write. Do not accept a client-submitted author or organization ID as proof of access. Retain the auth provider's cookie and CSRF protections for state-changing requests.

Protect the API independently#

If the page calls BTST, wire BTST's server authorization to the same application identity and policy. BTST v3 supplies named plugin operations and authorization contracts; the authorization guide explains how to integrate them.

Use the normal authorized operation surface for untrusted HTTP requests. A trusted server operation or low-level data getter exists for code that already owns the necessary authority. Moving a raw helper behind a public action without a policy check silently moves that trust boundary.

Client permission checks still have a useful role: hide unavailable actions, explain why publishing is disabled, and avoid sending requests that will predictably fail. The server must produce the same denial when a caller bypasses those controls.

Keep public loading independent of editor identity#

A published article should be readable by an anonymous visitor when that is the product's policy. Avoid making the public article wait for a browser-only session lookup before it renders. Keep request-specific QueryClient state separate from other users' requests, and keep private draft data out of public HTML and serialized loader results.

For an editor preview, use a request-aware loader and explicit private access. For a public article, constrain the query to published content. The draft and cache guide uses Next.js examples, but its distinction between private previews and public cached responses applies here too; the framework invalidation APIs differ.

Verify with more than one account#

Use anonymous, ordinary-member, and editor identities in a test environment. Request the draft URL directly, submit the save action directly, and call the underlying API. Repeat with a resource the editor should not control.

Then test a session transition. After sign-out, a stale page must not retain authority to save. After a role is removed, new server requests must apply the updated policy. If you use the Better Auth UI companion, the React Router integration refreshes route data through revalidator.revalidate() after session changes; it does not replace server authorization.

Record the response status and whether data changed for each test. A disabled button is useful UI evidence, while a denied request with an unchanged database is authorization evidence.

Evaluate the Blog feature for its publishing workflow, and use the current framework installation instructions to connect it to your existing React Router application.

In This Post

Define editorial permissions before wiring the pageA loader and an action each need a guardProtect the API independentlyKeep public loading independent of editor identityVerify with more than one account