Quickstart

Add authentication to your app in under 5 minutes.

1. Install the SDK

For Next.js:

npm install @bulwarkauth/nextjs
# or
pnpm add @bulwarkauth/nextjs

For plain React:

npm install @bulwarkauth/react

2. Get your publishable key

  1. Go to app.bulwarkauth.com
  2. Use the app switcher in the top navigation to select your application
  3. Navigate to Configure > API Keys
  4. Copy the pk_live_... key (or pk_test_... for development)

Each application in your workspace has its own publishable and secret key pair. Make sure you have the correct app selected before copying keys.

Publishable keys are safe to include in client-side code. They identify your app but cannot perform admin operations. See Publishable Keys for details.

Zero-config CORS: When your SDK sends the publishable key, CORS is handled automatically. No domains to configure — new subdomains, staging environments, and localhost all work out of the box. See CORS for details.

3. Wrap your app with BulwarkProvider

Next.js (app/layout.tsx):

import { CookieAwareBulwarkProvider } from "@bulwarkauth/nextjs";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CookieAwareBulwarkProvider publishableKey="pk_live_...">
          {children}
        </CookieAwareBulwarkProvider>
      </body>
    </html>
  );
}

React (src/main.tsx):

import { BulwarkProvider } from "@bulwarkauth/react";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <BulwarkProvider publishableKey="pk_live_...">
    <App />
  </BulwarkProvider>
);

4. Add sign-in and sign-up pages

// app/login/page.tsx
import { SignIn } from "@bulwarkauth/nextjs";

export default function LoginPage() {
  return <SignIn />;
}
// app/signup/page.tsx
import { SignUp } from "@bulwarkauth/nextjs";

export default function SignUpPage() {
  return <SignUp />;
}

5. Add UserButton to your header

// app/components/Header.tsx
"use client";
import { UserButton } from "@bulwarkauth/nextjs";

export function Header() {
  return (
    <header>
      <nav>
        <a href="/">Home</a>
        <UserButton afterSignOutUrl="/" />
      </nav>
    </header>
  );
}

UserButton renders an avatar with a dropdown for profile and sign-out. It renders nothing when the user is signed out.

6. Protect routes

Next.js middleware (middleware.ts at the project root):

import { createBulwarkMiddleware } from "@bulwarkauth/nextjs/middleware";

export default createBulwarkMiddleware({
  // Safe default: protectedPaths: ["/"] protects EVERY route, and
  // publicPaths lists the specific exceptions — rather than an allowlist
  // of protected routes, which silently leaves anything not listed public.
  protectedPaths: ["/"],
  publicPaths: ["/", "/login", "/signup"],
  loginPath: "/login",
  // Keeps users signed in past the ~15-min access-token TTL by silently
  // refreshing the session. Every token is verified against Bulwark's JWKS
  // regardless of whether this is set (see the nextjs SDK's "JWT
  // verification" section) — falls back to the same env var if omitted.
  bulwarkApiUrl: process.env.NEXT_PUBLIC_BULWARK_API_URL, // e.g. https://api.bulwarkauth.com
  // Required — Bulwark's JWKS is instance-wide, not per-tenant. As
  // written, this snippet requires BULWARK_TENANT_ID specifically: if it
  // is unset, createBulwarkMiddleware throws at construction
  // (fail-closed) rather than accepting a token from any tenant.
  // BULWARK_ALLOWED_TENANT_IDS is only consulted when you omit this
  // option entirely (then the SDK falls back to BULWARK_ALLOWED_TENANT_IDS,
  // then BULWARK_TENANT_ID, and throws if neither is set).
  allowedTenantIds: [process.env.BULWARK_TENANT_ID!],
});

export const config = {
  // ⚠️ Matcher exclusions are REGEX PREFIX matches — any path they skip
  // gets NO auth from this middleware. Exclude only exact static asset
  // paths, never a broad prefix or extension pattern — even
  // `_next/static/` alone is a prefix (e.g. /_next/static/settings, not
  // a real file, would otherwise fall through unauthenticated). See the
  // [nextjs SDK reference](/sdks/nextjs#config-options) for the full
  // matcher explanation.
  matcher: [
    "/((?!_next/static/.+\\.(?:js|css|map|woff2?|ttf|otf|eot|png|jpe?g|gif|svg|webp|avif|ico|txt|json)$|_next/image(?:/|$)|favicon\\.ico$).*)",
  ],
};

publicPaths is checked before protectedPaths, but the two are matched DIFFERENTLY: protectedPaths patterns match their whole subtree (/dashboard matches /dashboard/settings, never /dashboardx, and /x/* also matches /x itself), while publicPaths patterns match EXACTLY (/login exempts only /login, never /login/anything — a public subtree needs an explicit /x/*, which matches only paths STRICTLY under /x, never /x itself — list /x too if you want the base public as well). "/" also means something different in each list: it protects EVERY route in protectedPaths, but means ONLY the literal root page in publicPaths (never "everything"). loginPath is always implicitly public. Every pattern (in either list, or loginPath) must be a literal, plain-ASCII path — no %, no non-ASCII characters or spaces, no ./.. segments, and * only as a trailing /*//** wildcard on the whole pattern; this isn't glob or regex syntax. See the nextjs SDK reference for the full matching rules.

React (ProtectedRoute):

import { ProtectedRoute } from "@bulwarkauth/react";

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={
        <ProtectedRoute fallback={<Navigate to="/login" />}>
          <Dashboard />
        </ProtectedRoute>
      } />
    </Routes>
  );
}

7. Read the user in Server Components

// app/dashboard/page.tsx
import { auth } from "@bulwarkauth/nextjs/server";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  // auth() returns the verified session claims directly (BulwarkClaims:
  // sub, tenant_id, email, roles, exp, iat) or null — not { user, session }.
  const session = await auth();

  if (!session) {
    redirect("/login");
  }

  return <h1>Welcome, {session.email}</h1>;
}

Next steps