From 0.35 to 0.37
What to check when upgrading from better-supabase 0.35 to 0.36 or 0.37, where the proxy refreshes through @supabase/ssr, private entries ride in the App Shell, open pages follow role changes and stock counts fractional units.
This guide covers upgrading from 0.35 to 0.36 or to 0.37. Each section says which release brought it, so on 0.36 you can skip the 0.37 sections.
0.36.0 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. Several
blocks change their signatures too. 0.37.0 counts stock in fractional units
and adds brand templates, workflow lifecycles and timeline sources.
| Section | Release |
|---|---|
| Session refresh | 0.36.0 |
| Server clients and the token memo | 0.36.0 |
| Cache scopes | 0.36.0 |
| Role changes on open pages | 0.36.0 |
| AI chat | 0.36.0 |
| Blocks | 0.36.0 |
| Stock quantities, orders and shipments | 0.37.0 |
| Templates, lifecycles and timelines | 0.37.0 |
Run the codemod first, then work through the lines it lists:
npx better-supabase@latest codemod 0.37 --dry-run
npx better-supabase@latest codemod 0.37codemod 0.37 makes every rewrite for both releases. 0.36.0 shipped the
0.36 part as codemod 0.35; that command stays and makes the same rewrites,
without the 0.37 review lines.
Session refresh
These changes shipped in 0.36.0.
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.
| Before | After |
|---|---|
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
These changes shipped in 0.36.0.
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:
// before
const supabase = bsb.supabaseFor(auth);
// after
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.
| Before | After |
|---|---|
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
These changes shipped in 0.36.0.
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.
// before
const { session, db } = await bsb.cached();
cacheLife({ stale: sessionStale(session) });
// after
const { session, db } = await bsb.cached();| Before | After |
|---|---|
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
These changes shipped in 0.36.0.
SessionProvider takes session instead of sessionPromise, and the
codemod renames the prop. It accepts a resolved AuthSession or a promise.
// before
<SessionProvider sessionPromise={bsb.claims()}>{children}</SessionProvider>
// after
<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.
AI chat
These changes shipped in 0.36.0.
better-supabase upgrade moves the ai-chat module to version 5. It adds the
ai_chat_participants table and an author_id column on ai_messages, and
organization_id takes nulls when you set tenant: false. The new
storage options (messages: { key, tree, parts }) apply to new installs only; an existing install keeps a text
key, the tree and canonical parts.
| Before | After |
|---|---|
chats.create(organizationId: string, input) | chats.create(organizationId: string | null, input) |
projects.create(organizationId, input) | projects.create(organizationId | null, input, fields?) |
organizationId: string on chats, projects and moderation events | organizationId: string | undefined (a row outside a tenant) |
AssistantContext.organizationId: string | organizationId: string | null |
moderation.record(event) | moderation.record(event, fields?), with an optional organizationId |
| private chats: the owner only | the owner and the chat's participants |
chats.list(): the caller's own chats | the chats the caller owns or takes part in |
Code that read chat.organizationId as a string has to handle undefined,
and assistant hooks that take context.organizationId have to handle null,
for example by skipping tenant keys or a tenant quota.
Blocks
These changes shipped in 0.36.0. Run better-supabase upgrade and generate a
migration: state-machine moves to version 3, memory to 3, knowledge to 3,
inbox to 6 and workflow-editor to 4.
| Before | After |
|---|---|
transition(subject, to, reason) | transition(subject, to, { reason, actor }); the SQL takes actor jsonb |
validateGraph(graph, steps) | validateGraph(graph, { steps, nodes }) |
memory.archival.forget deletes the fact | soft deletes it; schedule archival.purge to remove it |
KnowledgeDocument.organizationId: string | string | undefined, undefined for a system document |
WorkflowNodeKind: the built-in kinds | any kind name; WorkflowBuiltinNodeKind lists the built-in ones |
revise creates a new quote | keeps the same quote at the next revision, and refuses an accepted one |
PublicQuote subtotal, taxTotal, total | totals, pricing and taxLines |
options.feed.reminders on calendar | tables.alarms |
The codemod rewrites a string literal reason passed to transition() and the
validateGraph call, and lists the other transition() calls, forget and
maps keyed by WorkflowNodeKind. The quote events carry revision instead
of revisionId. decide_quote, respond_to_quote and request_approval
take a trailing argument, so SQL that names their old signatures has to use
the new ones. save_reminder refuses a subject that doesn't exist in the
organization or that the caller can't read (REMINDER_SUBJECT_NOT_FOUND).
The changelog
has the full list for each block.
Stock quantities, orders and shipments
These changes ship in 0.37.0.
Stock is counted in numeric(20, 6) now, so a variant can count kilograms
or metres (ADR 0127).
Run better-supabase upgrade and generate the migration; existing stock keeps
its value and counts whole items until you call setStockItem.
| Before | After |
|---|---|
stock_levels.on_hand, reserved, available integer | numeric(20, 6), and available subtracts held |
before_stock_adjust(uuid, uuid, integer, text) | before_stock_adjust(uuid, uuid, numeric, text) |
before_stock_reserve(uuid, uuid, integer, text, text) | before_stock_reserve(uuid, uuid, numeric, text, text) |
move_stock(..., quantity integer, ...) | move_stock(..., quantity numeric, ..., detail jsonb) |
orderFromQuoteAction({ create }) | orderFromQuoteAction({ clients }), which calls orders.fromQuote |
order statuses without confirmed | confirmed, the first status of an order from lines or a quote |
| a shipment without stock | shipment.lines, taken out of inventory when the label is bought |
Redeclare your stock hooks with a numeric quantity: the block looks them
up by signature, so a hook still declared with integer is no longer
called. Code that switches over the order status needs a confirmed case.
The codemod lists these lines for review.
Templates, lifecycles and timelines
These changes ship in 0.37.0.
better-supabase upgrade adds a nullable brand column to templates, the
template_schedules and template_schedule_runs tables when reminders is
installed, and a workflow_version_id column to subject_lifecycles when a
machine sets workflow. Three unions gained members, so an exhaustive
switch over them stops compiling until it handles the new ones:
| Before | After |
|---|---|
TemplateScope: "tenant" | "system" | adds "brand", for a starter of one brand |
ActivitySource: event, audit, delivery, run | adds transition and message |
StatusState without workflow fields | adds workflow and workflowVersionId |
| a timeline lists status changes and comments only as events | also as transition and message entries; filter with sources |
Last updated on