Skip to content

Next.js + Vercel

Next.js on Vercel is an excellent match for Sesamy's BFF (cookie-based) auth pattern. Vercel automatically runs next.config.ts rewrites at its Edge Network, so the proxy hops that handle /auth/* and /api/* are resolved at a PoP close to the user — not back in a Node.js origin server — keeping latency low.

Why BFF?

The SPA (Auth0) flow stores tokens in localStorage, which is accessible to any JavaScript on the page. BFF keeps tokens server-side in HttpOnly cookies that JS can never read, eliminating the XSS token-theft surface entirely.

How it works

Browser          Vercel Edge          Sesamy API
  |                   |                    |
  |-- GET /auth/* --> |-- rewrite -------> |
  |<- Set-Cookie ---- |<------------------ |
  |                   |                    |
  |-- GET /api/* ---> |-- rewrite -------> |
  |<- data ---------- |<------------------ |

All of /auth/* and /api/* are transparently proxied at the Edge. Cookies are set on your own domain, so they are first-party — no custom domain or CNAME required.

Prerequisites

  • Next.js 14 or later (App Router or Pages Router both work)
  • A Vercel project (or a domain that resolves to your Next.js deployment)
  • BFF / Token Handler enabled for your vendor — contact Sesamy to activate this

1. Proxy auth and API routes

Add rewrites to next.config.ts so Vercel forwards both the Sesamy auth flow and the data API through your domain:

typescript
// next.config.ts
import type { NextConfig } from 'next';

const SESAMY_API = process.env.SESAMY_API_URL ?? 'https://api.sesamy.com';

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      // BFF auth — login, callback, logout, session check
      {
        source: '/auth/:path*',
        destination: `${SESAMY_API}/auth/:path*`,
      },
      // Sesamy data API — entitlements, contracts, products, …
      {
        source: '/api/:path*',
        destination: `${SESAMY_API}/:path*`,
      },
    ];
  },
};

export default nextConfig;

Vercel runs rewrites at its Edge Network by default — the request never reaches your Node.js origin, so there is no cold-start overhead on these proxy hops.

Conflicts with your own API routes

If your app also has Next.js Route Handlers under /api/*, the rewrite above will shadow them. Either scope the Sesamy rewrite to a sub-path (e.g. /api/sesamy/:path*${SESAMY_API}/:path*) and set api.endpoint to /api/sesamy in the sesamy-js config, or list your own routes first using the beforeFiles array:

typescript
async rewrites() {
  return {
    beforeFiles: [
      // your own API routes go here — they take precedence
    ],
    afterFiles: [
      { source: '/auth/:path*', destination: `${SESAMY_API}/auth/:path*` },
      { source: '/api/:path*',  destination: `${SESAMY_API}/:path*` },
    ],
  };
},

2. Configure sesamy-js

Point sesamy-js at the local proxy endpoints and enable cookie auth:

typescript
{
  clientId: process.env.NEXT_PUBLIC_SESAMY_CLIENT_ID,
  api: {
    endpoint: '/api',          // proxied to api.sesamy.com by Vercel Edge
  },
  auth: {
    useHttpCookies: true,      // activates BFF cookie plugin; no authPlugin needed
  },
}

The /auth/* path is used implicitly by sesamy-js for the login/logout/callback/userinfo flows — it always calls /auth/* relative to the current origin, regardless of api.endpoint.


3. Load sesamy-js in App Router

Option A — Scripts Host bundle (simplest)

If you use the Sesamy Scripts Host, the bundle already contains your vendor config. Just load the script:

tsx
// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://scripts.sesamy.com/s/YOUR_VENDOR_ID/bundle/stable.js"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Contact Sesamy to have your vendor's Scripts Host config updated to use api.endpoint: '/api' and auth.useHttpCookies: true.

Option B — npm package with inline config

Install the package:

bash
pnpm add @sesamy/sesamy-js
bash
npm install @sesamy/sesamy-js

Then inject the config as a JSON element (sesamy-js picks it up on load) and load the script:

tsx
// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const sesamyConfig = {
    clientId: process.env.NEXT_PUBLIC_SESAMY_CLIENT_ID,
    api: { endpoint: '/api' },
    auth: { useHttpCookies: true },
  };

  return (
    <html lang="en">
      <head>
        <script
          type="application/json"
          id="sesamy-js"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(sesamyConfig) }}
        />
      </head>
      <body>
        {children}
        <Script
          src="https://cdn.sesamy.com/sesamy-js/stable/sesamy-js.iife.js"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

Option C — Programmatic init

For full control, call init() from a Client Component:

tsx
// components/SesamyProvider.tsx
'use client';

import { useEffect } from 'react';
import { init } from '@sesamy/sesamy-js';

export function SesamyProvider() {
  useEffect(() => {
    init({
      clientId: process.env.NEXT_PUBLIC_SESAMY_CLIENT_ID!,
      api: { endpoint: '/api' },
      auth: { useHttpCookies: true },
    });
  }, []);

  return null;
}

4. Auth buttons (Client Component)

tsx
// components/AuthButton.tsx
'use client';

import { useEffect, useState } from 'react';

export function AuthButton() {
  const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);

  useEffect(() => {
    const onReady = async () => {
      setIsAuthenticated(await window.sesamy.auth.isAuthenticated());
    };

    window.addEventListener('sesamyJsReady', onReady);
    window.addEventListener('sesamyJsAuthenticated', () => setIsAuthenticated(true));
    window.addEventListener('sesamyJsLoggedOut', () => setIsAuthenticated(false));

    return () => {
      window.removeEventListener('sesamyJsReady', onReady);
    };
  }, []);

  if (isAuthenticated === null) return null;

  return isAuthenticated ? (
    <button onClick={() => window.sesamy.auth.logout()}>Log out</button>
  ) : (
    <button onClick={() => window.sesamy.auth.login()}>Log in</button>
  );
}

5. Gate content in Server Components

Use the Sesamy SDK server-side to check entitlements. Forward the session cookie from the incoming request so the SDK can authenticate on behalf of the user:

typescript
// app/article/[id]/page.tsx
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { client } from '@sesamy/sdk';

export default async function ArticlePage({ params }: { params: { id: string } }) {
  const cookieStore = await cookies();
  const sessionCookie = cookieStore.getAll()
    .map((c) => `${c.name}=${c.value}`)
    .join('; ');

  const sdk = client({ baseUrl: 'https://api.sesamy.com' });

  const { data, error } = await sdk.entitlements.list(
    {},
    { headers: { cookie: sessionCookie } },
  );

  if (error || !data?.some((e) => e.itemId === params.id)) {
    redirect(`/?paywall=${params.id}`);
  }

  return <article>{/* premium content */}</article>;
}

6. Vercel environment variables

Add these in Vercel → Project → Settings → Environment Variables:

VariableExampleExposed to browser
NEXT_PUBLIC_SESAMY_CLIENT_IDmy-vendor✅ yes
SESAMY_API_URLhttps://api.sesamy.com❌ no
SESAMY_API_KEYsk_…❌ no

SESAMY_API_URL lets you point at a different region or a local Worker during development without changing code:

env
# .env.local — used by `next dev`
NEXT_PUBLIC_SESAMY_CLIENT_ID=your-client-id
SESAMY_API_URL=https://api.sesamy.com   # or http://localhost:8787 for local Worker
SESAMY_API_KEY=your-management-api-key  # only needed for server-side entitlement checks

Further reading

Released under the MIT License.