# From 0.45 to 0.46

> What to check when upgrading from better-supabase 0.45 to 0.46, where PowerSync streams move to edition 3, attachments fill object_path from a trigger, doctor adds BS421, the rate limiter moves to its own entry and permission keys use the block name as their area.

Source: https://bettersupabase.com/docs/migration/0.45-to-0.46

0.46.0 renames permission keys so each one is the block name and a verb: run
`better-supabase codemod 0.46` and check the keys it lists for review. Old keys
keep working until the next minor, with a warning. The release also collects
fixes reported by an app in production. Most of them add options; four can
need a step: the regenerated module SQL, the PowerSync streams, a new doctor
error and an import path.

| Section                                                 | Needs code                                   |
| ------------------------------------------------------- | -------------------------------------------- |
| [Permission keys](#permission-keys)                     | Run `codemod 0.46`, then check `ai.*` keys   |
| [The SQL modules](#the-sql-modules)                     | Run `upgrade` and generate a migration       |
| [PowerSync streams](#powersync-streams)                 | Upload the regenerated streams               |
| [Access token hooks (BS421)](#access-token-hooks-bs421) | When doctor reports BS421                    |
| [The rate limiter import](#the-rate-limiter-import)     | Optional; the old import still works         |
| [Generated code](#generated-code)                       | Run `gen`; drop casts on `createdAt` inserts |

## Permission keys [#permission-keys]

Every permission key is now `<area>.<verb>`. The area is the block name in
snake_case (`contacts`, `workflow_editor`), and the verb is `read`,
`write`, `delete`, `manage` or a verb of the domain such as `approve` or
`publish`. A block installed as a named instance checks its own area: a
`tickets` instance of `tasks` checks `tickets.read`, not `tasks.read`.

Run the codemod. It renames the keys in strings in your code and config and in
quoted keys in your own SQL. It also moves code off the oauth-server names
0.44 deprecated (`createOAuthServer` and the `OAuth` types become `Oauth`):

```sh
npx better-supabase@latest codemod 0.46
```

| 0.45                                | 0.46                                               |
| ----------------------------------- | -------------------------------------------------- |
| `parties.*`                         | `contacts.*`                                       |
| `engagements.*`                     | `activities.*`                                     |
| `rules.read`, `rules.edit`          | `automations.read`, `automations.write`            |
| `rules.publish`, `rules.admin`      | `automations.publish`, `automations.manage`        |
| `controls.*`                        | `compliance.*`                                     |
| `support.start`, `support.read`     | `impersonation.start`, `impersonation.read`        |
| `support.revoke`                    | `impersonation.revoke`                             |
| `work_items.*`                      | `tasks.*`                                          |
| `targets.*`                         | `goals.*`                                          |
| `views.*`                           | `saved_views.*`                                    |
| `history.read`                      | `field_history.read`                               |
| `mail.*`                            | `mail_sync.*`                                      |
| `number_sequences.assign`           | `numbering.assign`                                 |
| `service_principals.*`              | `service_accounts.*`                               |
| `activity.read`                     | `timeline.read`                                    |
| `segments.edit`                     | `segments.write`                                   |
| `ai_chat.admin`                     | `ai_chat.manage`                                   |
| `custom_fields.sensitive.read`      | `custom_fields.read_sensitive`                     |
| `employees.view_sensitive`          | `employees.read_sensitive`                         |
| `workflow.edit`, `workflow.publish` | `workflow_editor.write`, `workflow_editor.publish` |
| `workflow.read`, `workflow.run`     | `workflows.*` and `workflow_editor.*` (below)      |
| `organization.export`               | `data_lifecycle.export`                            |
| `members.invite` (waitlist codes)   | `waitlist.invite`                                  |

The full list, with the wildcards, is `PERMISSION_KEY_RENAMES` and
`PERMISSION_KEY_SPLITS` in `better-supabase/blocks/permissions`. The same
entry exports `isOldPermissionKey(key)`, which tells whether a key was
renamed, and `currentPermissionKeys(key)`, which returns its new keys, for
code that checks keys stored outside better-supabase. `RENAMES_CODEMOD` names
the codemod that renames them.

The codemod lists some keys for review instead of guessing:

- The `ai.*` keys split into one area per AI block: `agents`, `ai_files`,
  `ai_keys`, `ai_schedules`, `connectors`, `knowledge` and `memory`. A
  check for `ai.read` becomes the read key of the block it guards, such as
  `knowledge.read`; a check for `ai.create` becomes `agents.write`,
  `ai_files.upload`, `ai_keys.use`, `ai_schedules.write`,
  `connectors.use` or `knowledge.write`.
- `workflow.read`, `workflow.run` and `workflow.admin` were shared by the
  workflows block and the workflow editor. Use `workflows.*` for runs and
  `workflow_editor.*` for definitions.
- `support.*` and `workflow.*` are also event patterns
  (`support.started`, `workflow.published`). Rename them only where they are
  permission wildcards.
- The waitlist block checks `waitlist.invite` instead of the invitations
  block's `members.invite`, and data exports check
  `data_lifecycle.export` instead of `organization.export`. Grant the new
  key where a custom role should keep the access.

Old keys keep working until the next minor. A role in
`blocks.permissions.roles` that lists one gets the new keys too, and
`permissionMatches` accepts both, with a warning once per key. Doctor reports
each old key it finds ([BS422](/docs/cli/doctor#bs422)).

With the catalog access model, `upgrade` renders a data step that renames the
rows in `permissions` and copies their role and override links to the new
keys. When the sessions module keeps authorization versions, the step bumps
every user's version so tokens pick up the new keys. With the `provider` or
`custom` models the keys live outside better-supabase: rename them in the
provider's roles, or in the functions your SQL defines, with the table above.

## The SQL modules [#the-sql-modules]

The attachments module moves to version 7. `object_path` was a generated
column, which Postgres logical replication skips, so PowerSync never received
it. It is now a plain column that a trigger fills on insert and update. An
existing install keeps its values: the module drops the generation expression
and leaves the stored paths in place. Attachments also broadcast `changed` to
`attachments:<tenant>` when a tenant's file changes (rename the topic with
`blocks.attachments.options.topic`).

The memberships module keeps custom roles in `membership_roles` under the
provider and custom access models, and a direct write of `memberships.role`
swaps the previous primary role instead of adding one. Regenerate the module
SQL and the migration:

```sh
npx better-supabase@latest upgrade
npx better-supabase@latest db diff -f upgrade_0_45
```

## PowerSync streams [#powersync-streams]

`better-supabase powersync streams` now writes Sync Streams edition 3. The
memberships, teams and attachments streams read the user's organizations
through a `with:` CTE, skip disabled memberships and sync every team in those
organizations. With `roles.many` a `membership_roles` stream follows.
Regenerate the file and upload it with the PowerSync dashboard or CLI:

```sh
npx better-supabase@latest powersync streams
```

The publication migration now starts with `create publication powersync for
table ...`, and a later run adds only the tables earlier migrations did not
publish. If you created the publication yourself (`for all tables`), delete
the first generated migration. [`powersync`](/docs/cli/powersync) covers the
details.

## Access token hooks (BS421) [#access-token-hooks-bs421]

Auth runs a custom access token hook as `supabase_auth_admin`, where
`auth.uid()` is null, so the restrictive `bsb_tenant_scope` policies hide every
row from it. Doctor now reports a `security invoker` hook that reads a
tenant-scoped table as [BS421](/docs/cli/doctor#bs421). List the role in the
tenant scope:

```ts title="better-supabase.config.ts"
export default defineConfig({
  database: { sql: { tenantScope: { exemptRoles: ["supabase_auth_admin"] } } },
});
```

## The rate limiter import [#the-rate-limiter-import]

`createRateLimit` and `rateLimited` have their own entry. The exports from
`better-supabase/blocks/queue` stay as deprecated aliases.

| 0.45                                                  | 0.46                                                |
| ----------------------------------------------------- | --------------------------------------------------- |
| `createRateLimit` from `better-supabase/blocks/queue` | `createRateLimit` from `better-supabase/rate-limit` |
| `rateLimited` from `better-supabase/blocks/queue`     | `rateLimited` from `better-supabase/rate-limit`     |

The oRPC and Hono `rateLimit` middleware take several limits through `hits`;
[Rate limits](/docs/rate-limit) shows both.

## Generated code [#generated-code]

Run `npx better-supabase@latest gen`. `records/` and `hooks.ts` now come out
the way oxfmt prints them, so a formatter run leaves them unchanged and you can
stop ignoring them. On tables the updated-at extension stamps, `createdAt` and
`updatedAt` are optional on `Insert`; remove any placeholder values or casts
you added to satisfy the old types. `gen.hooks.select` wraps every generated
select in a function of your own, for example to scope reads by tenant.