# From 0.14 to 0.15

> What to change when upgrading to better-supabase 0.15, where the config is grouped, the runtime instance is bsb, the SQL engine says block and the CLI has one command per job.

Source: https://bettersupabase.com/docs/migration/0.14-to-0.15

0.15 groups the config file, renames the runtime instance to `bsb`, moves
the SQL engine's names from module to block and gives the CLI one command per
job. `better-supabase codemod 0.15` makes the changes below in your config,
source files, `package.json` scripts and GitHub workflows. Run it, review the
diff, then run `better-supabase upgrade` and generate a migration.

```sh
npx better-supabase codemod 0.15
```

## The config is grouped [#the-config-is-grouped]

`better-supabase.config.ts` groups its keys into `database`, `auth`, `gen`,
`api` and `tooling`. `preset`, `blocks`, `plugins`, `records`, `tables` and
`adapters` stay at the top level. A config that still uses a 0.14 key fails
to load, and the message names the key's new place.

```ts title="better-supabase.config.ts"
export default defineConfig({
  database: { schemas: ["public"], sql: { owner: "tenant", api: "api" } },
  auth: { organization: { claim: "tenant_id" } },
  gen: { casing: "camel", output: "src/lib/supabase/generated.ts" },
  tooling: { doctor: { strict: true } },
  records: { customer: { table: "customers", title: "name" } },
  tables: { notes: { owner: "user", columns: { body: { type: "text" } } } },
  adapters: ["next"],
  blocks: { tenant: { names: { organization: "workspace" } } },
});
```

| Group      | Keys                                                                                                                                                                                                                        |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `database` | `source`, `schemas`, `sql`, `seed`, `expose`, `buckets`, `realtime`, `topics`, `vectorSearch`                                                                                                                               |
| `auth`     | `organization`, `claims`, `authorization`, `entitlements`                                                                                                                                                                   |
| `gen`      | `output`, `casing`, `postgrestVersion`, `generateFeatures`, `tables` (the generator overrides), `json`, `codecs`, `relations`, `sensitive`, `storagePaths`, `functions`, `generators`, `readSets`, `tasks`, `watchInterval` |
| `api`      | `openapi`, `specs`                                                                                                                                                                                                          |
| `tooling`  | `doctor`, `env`, `keys`, `skills`, `scaffold`                                                                                                                                                                               |

Some keys take new names on the way:

| 0.14                           | 0.15                       |
| ------------------------------ | -------------------------- |
| `subjects`                     | `records`                  |
| `appTables`                    | `tables`                   |
| `appTables.<name>.scope`       | `tables.<name>.owner`      |
| `integrations`                 | `adapters`                 |
| `tables` (generator overrides) | `gen.tables`               |
| `sql.scope`                    | `database.sql.owner`       |
| `claims.tenant`                | `auth.organization.claim`  |
| `plugins.tenant.column`        | `auth.organization.column` |
| `blocks.<name>.mode`           | `blocks.<name>.ownership`  |
| `blocks.<name>.scope`          | `blocks.<name>.owner`      |
| `blocks.<name>.entities`       | `blocks.<name>.names`      |
| `blocks.<name>.hooks`          | `blocks.<name>.callbacks`  |

Environment blocks (`$env`, `$ci` and the others) take the same groups. A
`better-supabase.config.json` is regrouped the same way. Create designs keep
`appTables` and `scope` in this release.

`resolveConfig` returns the same resolved config as in 0.14, so code that
reads `config.claims.tenant` or `config.appTables` from a resolved config
keeps working. `flattenConfig` and `groupConfig` from `better-supabase/config`
turn one shape into the other.

## The runtime instance is bsb [#the-runtime-instance-is-bsb]

The instance `createBetterSupabase` returns is called `bsb` in the docs, the
templates and the request contexts: `ctx.bsb`, `c.var.bsb`, `locals.bsb` and
`context.bsb`. The codemod renames `bs` where a file creates, imports or
destructures it. Database objects better-supabase installs use the `bsb_`
prefix; `upgrade` renames the old `bs_` ones.

## The SQL engine says block [#the-sql-engine-says-block]

`SqlModule` is `BlockSql`, `ModuleDefinition` is `BlockSqlDefinition`, and
the other `Module*` types from `better-supabase/sql` are `BlockSql*`.
`SubjectRef` is `RecordRef`, `entityForm` is `recordForm`, `ShipmentCarrier`
is `ShippingProvider` and `QueueBackend` is `QueueProvider`. Installed
versions live in `better_supabase.blocks`; `upgrade` copies the rows of
`better_supabase.modules` before the schema diff drops it.

## The CLI has one command per job [#the-cli-has-one-command-per-job]

The CLI keeps one command for each job: `init`, `add`, `remove`, `upgrade`,
`gen`, `doctor`, `db`, `dev`, `blocks`, `spec`, `mcp` and the helpers. The
commands below moved. Running an old one exits 2 and names the new one, and
the codemod rewrites them in `package.json` scripts and GitHub workflows.

| 0.14                          | 0.15                                          |
| ----------------------------- | --------------------------------------------- |
| `apply --preset <p>`          | `add --preset <p>`                            |
| `adopt <block>`               | `add --adopt <block>`                         |
| `sql add <block>`             | `add <block>`                                 |
| `sql sync`, `sql upgrade`     | `upgrade`                                     |
| `sql data`                    | `add --adopt`                                 |
| `sql list`                    | `blocks list --installed`                     |
| `sql print <block>`           | `blocks view <block> --sql`                   |
| `preset decode <code>`        | `blocks list --preset <name or URL>`          |
| `preset info`                 | `config --preset`                             |
| `introspect`                  | `gen --save-snapshot`                         |
| `seed`                        | `db seed`                                     |
| `env`, `run`, `keys`, `stack` | `dev env`, `dev run`, `dev keys`, `dev stack` |

`gen --out <file>` now names the file `gen --save-snapshot` writes;
the generated code still goes to `--out-dir`. `add --force` overwrites
files that exist, so a block whose hooks overlap the authorization provider's
needs `--allow-hook-overlap`. `remove` is new: it
takes blocks out of the config and `lib/blocks.ts` and deletes their module
files, and [`db diff`](/docs/cli/db#diff) writes the migration that drops
their tables. See the [CLI reference](/docs/cli) for every command.

## Designs replace preset codes [#designs-replace-preset-codes]

Create URLs and the CLI carry the design JSON instead of a preset code. The
config's `preset` key takes a preset name or an object with the conventions
that differ from the defaults and the blocks:

```ts title="better-supabase.config.ts"
// 0.14
preset: "1pWEF3d578B1iGt16",

// 0.15
preset: { casing: "camel", scope: "user", blocks: ["profiles", "tags"] },
```

A Create URL is `/create?preset=<name>` for an unchanged named preset and
`/create?design=<code>` otherwise, with the preset inside the design. A design
file is the design JSON with its `version` and `preset` keys, without the
0.14 `design` wrapper. `init --preset`, `add --preset` and `blocks list
--preset` take a name, a URL, a design code or a file, and stop on a 0.14 code
with a message that names the codemod. The codemod turns a code in the
config's `preset` into the name or object it holds, and rewrites
`*.design.json` files.

`encodePreset`, `decodePreset`, `isPresetCode`, `defaultPresetCode`,
`withPresetField`, `PRESET_BLOCKS`, `PRESET_FIELDS`, `PRESET_VERSION` and the
`DecodedPreset` type are gone from `better-supabase/preset`. Use
`presetSpec`, `presetFromSpec` and `presetOfSpec` for the config value,
`createUrl` for a link and `presetAndDesignOf` to read one.
`designConfigSource`, `designInstallCommand` and `designApplyCommand` merge
into `presetConfigSource`, `presetInstallCommand` and `presetApplyCommand`,
which take an optional design and are now async.

## Blocks take plain names [#blocks-take-plain-names]

26 blocks have new names. Each one changes in the config, the CLI, its
subpath, its exports, its operation names and its CloudEvent source:

```ts title="better-supabase.config.ts"
// 0.14
blocks: { tenant: {}, "work-items": {}, rules: {} },

// 0.15
blocks: { memberships: {}, tasks: {}, automations: {} },
```

| 0.14                 | 0.15                 |
| -------------------- | -------------------- |
| `work-items`         | `tasks`              |
| `parties`            | `contacts`           |
| `engagements`        | `activities`         |
| `targets`            | `goals`              |
| `number-sequences`   | `numbering`          |
| `actor`              | `created-by`         |
| `ensure-rls`         | `rls-guard`          |
| `grants`             | `data-api-grants`    |
| `shared-signals`     | `security-events`    |
| `service-principals` | `service-accounts`   |
| `support-sessions`   | `impersonation`      |
| `bulk`               | `bulk-actions`       |
| `controls`           | `compliance`         |
| `passes`             | `admission-passes`   |
| `ai-tasks`           | `ai-schedules`       |
| `ai-providers`       | `ai-keys`            |
| `workflow-builder`   | `workflow-editor`    |
| `jobs`               | `queue`              |
| `flags`              | `feature-flags`      |
| `access`             | `permissions`        |
| `rules`              | `automations`        |
| `identities`         | `linked-identities`  |
| `sessions`           | `session-revocation` |
| `tenant`             | `memberships`        |
| `activity`           | `timeline`           |
| `workflow-sdk-world` | `workflow-sdk`       |

The codemod renames the `blocks` keys and lists, the `preset.blocks` list,
the operation names in `auth.authorization.operations`, the imports from
`better-supabase/blocks/<name>` and the exports named after a block. It
lists the event sources `better-supabase/<name>` it finds, so you can update
your subscribers. A 0.14 name in the config stops `add` and `gen` with a
message that names the codemod.

Table names, permission keys, event types, hint codes and queue names keep
their 0.14 values. Run `better-supabase upgrade`, then
[`db diff`](/docs/cli/db#diff): the migration moves the installed blocks to
their new names, and renames two tables whose names clashed with a renamed
block. Hiring's `jobs` table becomes `job_openings`, and catalog's
`collections` table becomes `product_collections`.

## More blocks register record types [#more-blocks-register-record-types]

The quotes, invoices, orders, bookings and forms blocks register their primary
noun as a record type, so `records.quote`, `records.invoice`, `records.order`,
`records.booking` and `records.form` now fail with `config_invalid`. Remove the
entry when it points at the block's own table. When it points at a table of
your own, keep it and turn the block's record type off:

```ts title="better-supabase.config.ts"
blocks: {
  bookings: { names: { booking: { subject: false } } },
},
```

Then run `better-supabase upgrade`. Every block that owns tables now declares
its nouns, so `blocks.<name>.names` renames them; see
[SQL blocks](/docs/blocks/sql).

## Plugins become block extensions [#plugins-become-block-extensions]

The built-in plugins live with the blocks that own their columns, and the
`plugins` config key goes. The codemod moves the config and the imports:

| 0.14                                                      | 0.15                                                                                            |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `plugins.timestamps`                                      | the `updated-at` block (`createdAt` and `updatedAt` options)                                    |
| `plugins.actor`                                           | the `created-by` block (`createdBy`, `updatedBy` and `impersonatedBy`)                          |
| `plugins.softDelete.column`                               | the `trash` block's `deletedAt` option                                                          |
| `plugins.tenant.column`                                   | `auth.organization.column`                                                                      |
| `timestamps()` from `better-supabase/plugins/timestamps`  | `updatedAtExtension()` from `better-supabase/blocks/updated-at`                                 |
| `actor()` from `better-supabase/plugins/actor`            | `createdByExtension()` from `better-supabase/blocks/created-by`                                 |
| `softDelete()` from `better-supabase/plugins/soft-delete` | `trashExtension()` from `better-supabase/blocks/trash`                                          |
| `tenant()` from `better-supabase/plugins/tenant`          | `membershipsExtension()` from `better-supabase/blocks/memberships`                              |
| `rules()`, `safe()`, `recommended()`, `strict()`          | `guards()`, `safeGuards()`, `recommendedGuards()`, `strictGuards()` from `better-supabase/lint` |
| `validation()` from `better-supabase/plugins/validation`  | `validation()` from `better-supabase`                                                           |

`gen` now installs the extensions of the installed blocks in the generated
schema, and `validation()` with the generated validators when a `zod()`,
`valibot()` or `standardSchema()` generator is configured. Remove the
`.use()` calls the codemod moved unless they pass options; a `.use()` of an
extension with options replaces the one the schema installed.

One default changed: a query on an organization table without an
organization is left to RLS (`onMissing: "skip"`). The codemod keeps the
0.14 behavior by writing `auth.organization.onMissing: "error"` for a config
that had `plugins.tenant`; set it yourself to fail closed.

## One data contract for tables and blocks [#one-data-contract-for-tables-and-blocks]

Repositories and block clients share the data verbs: `list`, `page`, `get`,
`count` and `aggregate` read, `create`, `update`, `upsert` and `delete`
write, and `attach` and `detach` link a row to a record. The codemod
rewrites the repository calls:

| 0.14                                           | 0.15                                                 |
| ---------------------------------------------- | ---------------------------------------------------- |
| `findMany(args)`                               | `list(args)`                                         |
| `paginate(args)`                               | `page(args)`                                         |
| `findById(id)`                                 | `get(id)`, or `get(id, { optional: true })` for null |
| `findUnique(key)`, `findOnly(args)`            | `get(key)`, or `get({ where })` for a filter         |
| `findFirst(args)`                              | `list({ ...args, limit: 1 })`, then the first row    |
| `exists(args)`                                 | `count(args)`, then compare with zero                |
| `createMany(rows)`, `upsertMany(rows)`         | `create(rows)`, `upsert(rows)` with an array         |
| `updateMany(where, patch)`                     | `update({ where }, patch)`                           |
| `deleteMany(where)`                            | `delete({ where })`                                  |
| `guards({ noUnboundedFindMany })`              | `guards({ noUnboundedList })`                        |
| `guards({ noDeleteManyWithoutWhere })`         | `guards({ noDeleteWithoutWhere })`                   |
| `better-supabase/no-unbounded-find-many`       | `better-supabase/no-unbounded-list`                  |
| `better-supabase/no-delete-many-without-where` | `better-supabase/no-empty-where`                     |

`get` takes the primary key, an object with one complete unique key, or
`{ where }`. With `{ where }` it fails with `multiple_rows` when more than
one row matches, so it replaces `findOnly`. Read sets and query specs have
no `.map`, so where the codemod wrote `findFirst` as a list with
`.map((rows) => rows[0] ?? null)`, use `get({ where }, { optional: true })`
or destructure the first row; the codemod lists those calls.

The lint rule `no-empty-where` reports an `update` or `delete` whose filter is
`{ where: {} }`, and `no-unbounded-list` reports a `list` without `limit`.

Block clients move to the same verbs. These calls are listed for review,
because the names are too common to rewrite safely:

| 0.14                                                   | 0.15                                                      |
| ------------------------------------------------------ | --------------------------------------------------------- |
| `tags.tag(record, tagId)`, `tags.untag(record, tagId)` | `tags.attach(record, tagId)`, `detach`                    |
| `tags.create(kind, name, color, description, fields)`  | `tags.create({ kind, name, color, description, fields })` |
| `tags.update(id, name, fields)`                        | `tags.update(id, { name, fields })`                       |
| `tags.list(kind)`                                      | `tags.list({ kind })`                                     |
| `forSubject(record)` on tags, channels and scores      | `list({ on: record })`                                    |
| `save(...)` on a block client                          | `upsert(...)` with the same arguments                     |
| `set(...)` on ai-cache and on ai-files `providerFiles` | `upsert(...)`                                             |

The rule builder and saved-view controllers keep `save`, the settings client
keeps `setUser` and `setOrganization`, and domain verbs such as `approve`,
`send` and `publish` stay.

## Blocks install as instances [#blocks-install-as-instances]

Each key in `blocks` is now an instance. A config keyed by block name keeps
working unchanged. To install a block twice, add a key with `block` set to
the block's name; when a block depends on a block you installed twice, name
the instance it uses in `uses`. See [Instances](/docs/blocks#instances).
`resolveConfig` reports a core block, such as `memberships`, that is renamed
or installed twice.

## Every instance renders its own SQL [#every-instance-renders-its-own-sql]

`better_supabase.blocks` has a new `block` column. The schema file creates it
for new installs, and the data file of a second instance adds it with
`alter table ... add column if not exists`, so an existing database needs no
manual step. Rows written before have `block` set to null, which means the
row's `name` is the block. Block markers keep their format; a second instance
adds its block after the mode (`-- @bsb-block tickets@1 managed tasks`).