How to Handle Authentication and Protected Routes in a Gatsby Application

Use wrapRootElement in gatsby-browser.js to inject an authentication provider, store session state in localStorage, and configure client‑only routes via matchPath in gatsby-node.js to gate content behind a custom PrivateRoute component.

Gatsby generates static HTML at build time, but you can still handle authentication and protected routes in a Gatsby application by leveraging client‑only routes and React context. The gatsbyjs/gatsby repository provides official patterns and examples—such as the Auth0 integration in examples/functions-auth0 and the authentication tutorial—that demonstrate how to secure pages without sacrificing static performance.

Understanding Client‑Only Routes for Authentication

Static sites cannot rely on server‑side sessions, so Gatsby uses client‑only routes to handle dynamic, user‑specific URLs. These routes are created at runtime in the browser rather than at build time, allowing you to intercept paths like /app/* and render React components that check authentication state.

Configuring matchPath in gatsby-node.js

To enable client‑only routing, you must tell Gatsby to treat a specific path prefix as a wildcard. In gatsby-node.js, use the onCreatePage API to assign a matchPath property to the page:

// gatsby-node.js
exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions

  // Any path beginning with /app is handled by the app shell page.
  if (page.path.match(/^\/app/)) {
    page.matchPath = "/app/*"
    createPage(page)
  }
}

Source: docs/tutorial/authentication-tutorial.md in the gatsbyjs/gatsby repository.

Setting Up an Authentication Provider

Because Gatsby builds static HTML, authentication SDKs (like Auth0 or Firebase) must only execute in the browser. The standard pattern is to wrap your application in an authentication provider using the wrapRootElement API in gatsby-browser.js (and optionally gatsby-ssr.js to prevent hydration mismatches).

Wrapping the Root Element in gatsby-browser.js

The wrapRootElement export allows you to inject a React context provider around the entire component tree. For Auth0, this looks like:

// gatsby-browser.js
import * as React from "react"
import { navigate } from "gatsby"
import { Auth0Provider } from "@auth0/auth0-react"

const onRedirectCallback = (appState) => {
  navigate(appState?.returnTo || "/")
}

export const wrapRootElement = ({ element }) => (
  <Auth0Provider
    domain={process.env.GATSBY_AUTH0_DOMAIN}
    clientId={process.env.GATSBY_AUTH0_CLIENT_ID}
    audience={process.env.GATSBY_AUTH0_AUDIENCE}
    scope={process.env.GATSBY_AUTH0_SCOPE}
    redirectUri={window.location.origin}
    onRedirectCallback={onRedirectCallback}
  >
    {element}
  </Auth0Provider>
)

Source: examples/functions-auth0/gatsby-browser.js in the gatsbyjs/gatsby repository.

Handling Server‑Side Rendering Considerations

When using wrapRootElement in gatsby-ssr.js, ensure the provider does not access browser‑only globals like window during the server render. The Auth0 example above references window.location.origin, so it should only run in gatsby-browser.js. For universal rendering, guard browser‑specific logic with a check like typeof window !== "undefined".

Creating a Protected Route System

With the provider in place, you need a mechanism to check login status and redirect unauthenticated users. Because Gatsby pages are static, this check must happen at runtime inside a shell page that uses @reach/router (the router Gatsby uses internally).

Building a Client‑Side Auth Service

Create a lightweight service to persist session state across refreshes. The Gatsby tutorial provides a minimal implementation using localStorage:

// src/services/auth.js
export const isBrowser = () => typeof window !== "undefined"

export const getUser = () =>
  isBrowser() && window.localStorage.getItem("gatsbyUser")
    ? JSON.parse(window.localStorage.getItem("gatsbyUser"))
    : {}

const setUser = user =>
  window.localStorage.setItem("gatsbyUser", JSON.stringify(user))

export const handleLogin = ({ username, password }) => {
  if (username === `john` && password === `pass`) {
    setUser({
      username: `john`,
      name: `Johnny`,
      email: `johnny@example.org`,
    })
    return true
  }
  return false
}

export const isLoggedIn = () => {
  const user = getUser()
  return !!user.username
}

export const logout = callback => {
  setUser({})
  callback()
}

Source: docs/tutorial/authentication-tutorial.md in the gatsbyjs/gatsby repository.

Implementing PrivateRoute Components

Combine the auth service with a router to guard specific paths. In your shell page (e.g., src/pages/app.js), use the Router from @reach/router and conditionally render components based on isLoggedIn():

// src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Layout from "../components/layout"
import Profile from "../components/profile"
import Login from "../components/login"
import { isLoggedIn } from "../services/auth"
import { navigate } from "gatsby"

const PrivateRoute = ({ component: Component, ...rest }) => {
  if (!isLoggedIn()) {
    navigate("/app/login")
    return null
  }
  return <Component {...rest} />
}

const App = () => (
  <Layout>
    <Router>
      <PrivateRoute path="/app/profile" component={Profile} />
      <Login path="/app/login" />
    </Router>
  </Layout>
)

export default App

When a user visits /app/profile, the PrivateRoute component checks isLoggedIn(). If the user is unauthenticated, navigate redirects them to /app/login before the protected component ever renders.

Summary

  • Use wrapRootElement in gatsby-browser.js to inject authentication providers like Auth0 only in the browser, avoiding server‑side rendering errors.
  • Persist sessions with localStorage via a custom service (e.g., src/services/auth.js) so login state survives page refreshes.
  • Enable client‑only routes by setting page.matchPath = "/app/*" inside onCreatePage in gatsby-node.js, allowing dynamic routing under a specific path prefix.
  • Guard protected content with a PrivateRoute component that checks isLoggedIn() and uses Gatsby’s navigate helper to redirect unauthenticated users before rendering sensitive UI.

Frequently Asked Questions

Can Gatsby handle server‑side authentication?

No. Because Gatsby generates static HTML at build time, it cannot maintain server‑side sessions or validate JWTs on the server. Authentication must happen entirely in the browser using client‑side JavaScript, and protected data should be fetched from secure API endpoints that validate tokens at request time.

How do I persist login state across page refreshes in Gatsby?

Store the user object or token in localStorage (or sessionStorage) via a helper service like src/services/auth.js. On app initialization, read from storage to set the initial auth state. This ensures the user remains logged in after refreshing the page or closing and reopening the browser tab.

What is the difference between createPages and client‑only routes for authentication?

createPages generates static HTML pages at build time for known routes (e.g., /about). Client‑only routes (configured via matchPath in onCreatePage) tell Gatsby to serve a single "shell" page for any URL matching a pattern (e.g., /app/*), allowing React to handle routing dynamically at runtime—essential for authenticated routes that cannot be statically generated.

How do I prevent a flash of unauthenticated content in Gatsby?

Check authentication status before mounting protected components. In your PrivateRoute component, return null or a loading spinner while verifying the session (e.g., checking localStorage or waiting for an auth SDK to initialize). Only render the protected UI once isLoggedIn() confirms the user is authenticated, preventing the protected content from flashing before the redirect completes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →