From 0.34 to 0.35
What to check when upgrading to better-supabase 0.35, where the proxy refreshes through @supabase/ssr, private entries ride in the App Shell and open pages follow role changes.
0.35 changes how sessions refresh and how long cached session data lives.
The proxy and every adapter's middleware refresh through @supabase/ssr on
every request they match. bsb.cached() entries stay fresh for 300 seconds
instead of the token's lifetime. A page can read the session, the
authorization version and its data in one database call, and an open page
picks up a role change without waiting for the token to rotate. Run the
codemod first, then work through the lines it lists:
npx better-supabase@latest codemod 0.35 --dry-run
npx better-supabase@latest codemod 0.35Session refresh
The proxy (bsb.proxy) and the middleware of the other adapters refresh
through createServerClient from @supabase/ssr, prefetches included.
Parallel requests with the same refresh token share one call to Auth.
| 0.34 | 0.35 |
|---|---|
refresh: "navigation" | refresh: true (the default), or a function of the request |
verifySession: "navigation" | verifySession: "document" (isDocumentLoad) |
onExpiredPrefetch on bsb.proxy() | removed: a prefetch refreshes, so protect never sees it expired |
shouldRefresh | removed; shouldCheckSession stays in better-supabase/next |
clearSessionCookies(request, response) | await clearSessionCookies(request, response) |
writeSession, refreshSession, pruneSessionChunks, sessionEncoding, clearSessionAtScopes, AUTH_CACHE_HEADERS | removed: @supabase/ssr writes the cookies and their cache headers |
readSession and parseCookies stay in better-supabase/ssr. The codemod
rewrites the two option values and lists the rest.
A token whose iss names another project is now rejected with
INVALID_ISSUER. Set auth.issuer to require an exact issuer, or false to
skip the check. The JWKS is fetched with auth.fetch and cached once per
process, so the proxy, the render and the route handlers share one copy. When
Auth can't be reached, a refresh fails within timeoutMs and keeps the
cookies as they are.
Server clients and the token memo
The server builds its Supabase clients with createContextClient and
createAdminClient from @supabase/server. A user's client sends the token
to PostgREST, Storage and Realtime; the secret-key client is created on first
use and shared. bsb.supabaseFor(auth, headers?) is now
bsb.contextClient(auth, headers?), and the codemod renames the calls:
// 0.34
const supabase = bsb.supabaseFor(auth);
// 0.35
const supabase = bsb.contextClient(auth);Each server keeps one memo of verified tokens. The proxy, bsb.context(),
getClaims() and bsb.snapshot() look tokens up in it, so a token is
verified once while it is valid, and bsb.sessions.evict(userId) reaches
every path. The memos inside resolveAuth and accessTokenResolver are
gone: called on their own, without createServer, they verify every token.
sessions: false now turns off all reuse, so each request verifies again
(about 0.1 ms); the codemod lists it for review.
| 0.34 | 0.35 |
|---|---|
bsb.supabaseFor(auth, headers?) | bsb.contextClient(auth, headers?) |
sessions: false skips the session memo | sessions: false verifies every token on every call |
accessTokenResolver remembers tokens | it verifies each request; put it behind createServer |
Cache scopes
bsb.cached() sets stale to 300 seconds, so private entries ride in the
App Shell. The proxy refreshes every request, so the token's lifetime no
longer caps the entry; life: { capToToken: true } brings the cap back. In
development a stale below 300 seconds warns once.
// 0.34
const { session, db } = await bsb.cached();
cacheLife({ stale: sessionStale(session) });
// 0.35
const { session, db } = await bsb.cached();| 0.34 | 0.35 |
|---|---|
sessionStale, SessionStaleOptions | removed: bsb.cached() sets the stale time |
bsb.cached({ life: { min, max } }) | bsb.cached({ life: { stale, capToToken } }) |
bsb.contextFromSession(session, { token }) | bsb.contextFromSession(await bsb.getClaims()) |
New in the same area:
bsb.remote(fn, { share, tenant })andbsb.cached({ scope: "remote", key })read user-scoped data in"use cache: remote", keyed by the user, Auth session, tenant and authorization version, never by the token (remote scope).bsb.bootstrap({ rpc, args, tenant, redirect })calls thebootstrap_<rpc>wrapper thatblocks.sessions.options.bootstrapgenerates: one query for the session check, the authorization version and the page's data (one database call per page).proxy({ verifySession: "bootstrap" })then skips the proxy's Auth call.bsb.getAccess(source?)returns the caller's context, session, tenant, authorization version andcan(permission)from the request, aSessionSnapshotor a bootstrap result.createNext({ rootParams: { locale, brand, tenant } })addsbsb.locale(),bsb.brand()andbsb.tenant().postAuthPath(source, fallback)reads a safe?next=path.toNextRoute(entries, handler)andtoNextAction(entries, action)run@supabase/middlewareentries around a route handler or Server Action.
The island pattern puts these together on one page.
Role changes on open pages
SessionProvider takes session instead of sessionPromise, and the
codemod renames the prop. It accepts a resolved AuthSession or a promise.
// 0.34
<SessionProvider sessionPromise={bsb.claims()}>{children}</SessionProvider>
// 0.35
<SessionProvider session={bsb.claims()}>{children}</SessionProvider>To follow role and grant changes, turn on authzVersions in the sessions
block and give the provider a browser client:
blocks: { sessions: { options: { authzVersions: true } } },<SessionProvider
session={bsb.claims()}
client={supabase}
onAuthzChange={async () => {
await refreshSessionCache();
router.refresh();
}}
/>The option adds better_supabase.user_authz_versions, the
bump_authz_version() trigger function, bump_authz_versions(uuid[]) and
authz_version(uid) for the access token hook. A bump broadcasts on
AUTHZ_TOPIC for servers and on the private authz:<uid> topic, which only
that user may read. The visible tab refreshes the token and calls
onAuthzChange, then tells the other tabs over a BroadcastChannel.
refreshSessionCache is your re-export of bsb.refreshSessionCache() from a
"use server" file; it drops the caller's own bsb.cached() entries.
subscribeAuthzVersions defaults topic, event and userIdKey to what
authzVersions sends. Authorization providers can add their own cache tags
with the optional sessionTags(userId) hook, and evictSession, sign-out and
refreshSessionCache clear them; apiVersion: 1 stays.
sessionStatusRoute now defaults to verifySession: false and reads the
verified claims only. Pass verifySession: true to ask Auth on every check.
Last updated on