# 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.

Source: https://bettersupabase.com/docs/migration/0.35-to-0.37

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](#session-refresh)                                              | 0.36.0  |
| [Server clients and the token memo](#server-clients-and-the-token-memo)          | 0.36.0  |
| [Cache scopes](#cache-scopes)                                                    | 0.36.0  |
| [Role changes on open pages](#role-changes-on-open-pages)                        | 0.36.0  |
| [AI chat](#ai-chat)                                                              | 0.36.0  |
| [Blocks](#blocks)                                                                | 0.36.0  |
| [Stock quantities, orders and shipments](#stock-quantities-orders-and-shipments) | 0.37.0  |
| [Templates, lifecycles and timelines](#templates-lifecycles-and-timelines)       | 0.37.0  |

Run the codemod first, then work through the lines it lists:

```sh
npx better-supabase@latest codemod 0.37 --dry-run
npx better-supabase@latest codemod 0.37
```

`codemod 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 [#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 [#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:

```ts
// 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 [#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.

```ts
// 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 })` and `bsb.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](/docs/frameworks/next-cache-components#remote-scope)).
- `bsb.bootstrap({ rpc, args, tenant, redirect })` calls the
  `bootstrap_<rpc>` wrapper that `blocks.sessions.options.bootstrap`
  generates: one query for the session check, the authorization version and
  the page's data
  ([one database call per page](/docs/frameworks/next-cache-components#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 and `can(permission)` from the request, a
  `SessionSnapshot` or a bootstrap result.
- `createNext({ rootParams: { locale, brand, tenant } })` adds `bsb.locale()`,
  `bsb.brand()` and `bsb.tenant()`.
- `postAuthPath(source, fallback)` reads a safe `?next=` path.
- `toNextRoute(entries, handler)` and `toNextAction(entries, action)` run
  `@supabase/middleware` entries around a route handler or Server Action.

The [island pattern](/docs/frameworks/next-islands) puts these together on
one page.

## Role changes on open pages [#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.

```tsx
// 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:

```ts title="better-supabase.config.ts"
blocks: { sessions: { options: { authzVersions: true } } },
```

```tsx
<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 [#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](/docs/blocks/ai-chat#message-storage) (`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 [#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](https://github.com/ScaleDockHQ/better-supabase/blob/main/packages/better-supabase/CHANGELOG.md#0360)
has the full list for each block.

## Stock quantities, orders and shipments [#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](https://github.com/ScaleDockHQ/better-supabase/blob/main/docs/decisions/0127-fractional-stock-lots-and-allocations.md)).
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 [#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` |