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.
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.
npx better-supabase codemod 0.15The 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.
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 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
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 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 writes the migration that drops
their tables. See the CLI reference for every command.
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:
// 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
26 blocks have new names. Each one changes in the config, the CLI, its subpath, its exports, its operation names and its CloudEvent source:
// 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: 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
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:
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.
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
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.
Last updated on