Skip to content

Next.js

@potalab/base/next keeps the session (access_token, refresh_token, expires_at) in one cookie shared by the browser client, the server client and the proxy.

Terminal window
pnpm add @potalab/base
"use client"
import { createBrowserClient } from "@potalab/base/next"
import type { Database } from "@/db"
export const base = () =>
createBrowserClient<Database>({ url: process.env.NEXT_PUBLIC_POTALAB_BASE_URL!, key: process.env.NEXT_PUBLIC_POTALAB_BASE_KEY! })

You get one instance per URL and key. It refreshes the token 60 s before it expires and writes the new session to the cookie.

Server (Server Components, Route Handlers, Server Actions)

Section titled “Server (Server Components, Route Handlers, Server Actions)”

Create one client per request. A module-level client would share one user’s session with every other request.

import { cookies } from "next/headers"
import { createServerClient } from "@potalab/base/next"
export async function baseServer() {
const store = await cookies()
return createServerClient<Database>({
url: process.env.NEXT_PUBLIC_POTALAB_BASE_URL!,
key: process.env.NEXT_PUBLIC_POTALAB_BASE_KEY!,
cookies: {
getAll: () => store.getAll(),
setAll: (list) => list.forEach((c) => store.set(c.name, c.value, c.options)),
},
})
}

Server Components cannot write cookies: the write is skipped and the proxy takes care of the refresh. For admin work on the server use a secret key with createClient from @potalab/base, and never pass it the user’s cookies.

In proxy.ts (Next 16 and later; middleware.ts in earlier versions):

import { NextResponse, type NextRequest } from "next/server"
import { updateSession } from "@potalab/base/next"
export async function proxy(request: NextRequest) {
const { response, user } = await updateSession(request, {
url: process.env.NEXT_PUBLIC_POTALAB_BASE_URL!,
key: process.env.NEXT_PUBLIC_POTALAB_BASE_KEY!,
next: (init) => NextResponse.next(init),
})
if (!user && request.nextUrl.pathname.startsWith("/app")) {
return NextResponse.redirect(new URL("/login", request.url))
}
return response
}
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] }

Each project gets its own cookie: base-<projectRef>-auth when you pass projectRef, otherwise a name derived from the key. If your proxy covers several projects, use updateSessions instead of repeated updateSession calls so all cookies land on a single response:

const shop = { url, key: SHOP_KEY, projectRef: "my-shop" }
const blog = { url, key: BLOG_KEY, projectRef: "blog" }
const { response, projects } = await updateSessions(request, [shop, blog], { next: (init) => NextResponse.next(init) })

Each cookie is about 700 bytes and is sent on every request, so keep the number of projects per host small.

  • The cookie is not HttpOnly, because the browser client has to read it, so an XSS bug could read the refresh token.
  • A browser refresh and a proxy refresh can overlap and revoke the session with refresh_token_reused, because the server has no reuse grace window yet.
  • On Next 14, fetch results are cached by default. Pass fetch: (u, i) => fetch(u, { ...i, cache: "no-store" }).