Changelog
Every better-supabase release with its changes, newest first, built from the CHANGELOG.md at the repository root.
0.8.0 (2026-10-12)
-
New
better-supabase/arazzosubpath:createArazzoandarazzoFormatrender Arazzo 1.0 and 1.1 workflows from the API model, with source descriptions that point at the OpenAPI and AsyncAPI files.defineWorkflowtypes each step by theoperationIdit calls, andpaginationWorkflows,createThenGetWorkflowsandauthWorkflowbuild common workflows from your resources' operations. -
New
better-supabase/asyncapisubpath:createAsyncApiandasyncapiFormatrender AsyncAPI 3.0 and 3.1 from the same API model as OpenAPI. The document describes Realtime table changes and broadcast topics on the websocket server, CloudEvents with a header trait (CLOUD_EVENT_TRAIT) and adataschema, outgoing webhooks, and the events that resource actions emit. -
audit_eventandaudit_event_trustedtakesupport_session,impersonated_byandimpersonation_reason, stored ahead of the values in the caller'sactclaim. The service role may pass any values; another caller only itself asimpersonated_by, from a token without anactclaim.sql syncdrops the previous signatures. Module actions pass them throughModuleAudit.supportSession,impersonatedByandimpersonationReason, so thesupport-sessionsmodule'ssupport.startedandsupport.endedentries now carry the session id, the admin and the reason in the impersonation columns instead of only in metadata.AuditEventInputgainssupportSessionId,impersonatedByandimpersonationReason, andauditEventOffills them forsupport.*events fromsessionId,adminIdandreason. -
createServertakes anauthorizer: a runtime decision point (apiVersion: 1) that receives each request in the AuthZEN information model (subject, action, resource and context) and returnsgranted,deniedorapproval-required. better-supabase builds the subject from the verified request only (subject.properties.claimscarries a read-only copy of the verified access-token claims, so an adapter can read memberships and roles without a database read) and refuses anything butgranted.defineAuthorizerand its types (Authorizer,AnyAuthorizer,AuthorizationRequest,AuthorizationOutcome,AuthorizationSubject,AuthorizationResource,PermissionRef) are exported frombetter-supabase/server, andbetter-supabase/testingaddstestAuthorizer,staticAuthorizerand thetestDocumentFormatconformance kit.Routes, actions,
bs.requireand the guards in every adapter take apermissionoption that the authorizer checks.defineResourceaddshooksthat run around the repository call andactions(defineAction) on an item or a collection, served next to the collection and item routes by Honoresource(), edgeresources()and the new Next.jsbs.resources(), which returns the handlers for a catch-all route. MCP tools take apermissiontoo, andtools/listhides the tools the authorizer denies. -
The CLI reads its options from
better-supabase.config.ts, sobetter-supabase genis the one command a project runs.genruns every generator the config sets up, in order: the typed client,sql sync,spec emit, the seed client,scaffold apiand, when listed,env.gen.tasksor--taskspicks the list,gen --checkchecks them all, andgen --watchreruns the steps whose inputs changed.gen.watchIntervalsets the watch interval.- New config keys give each repeated flag a default:
env.outputandenv.prefix,doctor.only,doctor.formatanddoctor.output,specs.manifest,scaffold.api,skills.agentsandkeys.output. A flag still wins for one run. $env.<name>and the$development,$production,$testand$ciblocks override the config per environment. The CLI applies the one named by the new global--envflag, then$BETTER_SUPABASE_ENV, thenciwhen$CIis set.better-supabase configshows the active environment.initrecords the integrations it set up underintegrations, andaddwithout an argument sets up the listed ones whose files are missing.scaffold apiandaddprint the config entry for the flags you passed.
-
A
Dbcarries Storage as the same caller:db.$storageis the newstorageoption ofconnect(client, context, { storage }), or the client'sstorage, and it throws on a connection with neither.bucket.connect(db.$storage)and blocks givendb.$storagethen follow whatever theDbruns as. In a support session,ctx.db.$storageandctx.sql.$storagearectx.storage, which is now typedStorageApi, so the same code reads Storage as the caller in a normal request and as the target in a support session. -
dbTransport(db, { schema })frombetter-supabase/blocks(and every block entry) calls block functions through aDb's executor, so a block follows whatever theDbruns over: the Data API forctx.dbin a normal request, and direct Postgres as the target user in a support session or for an API key. It rejects with the raw database error likerpcTransportandsqlTransport, so block error mappers see the same codes and hints, and aDbErrorthat a transport rejects with now passes through block calls unchanged. -
Table and column comments now reach the generated meta as
descriptionanddeprecated, and the JSON Schema output marks deprecated tables and columns.jsonSchema()takes atarget(draft-2020-12,draft-07oropenapi-3.0, typed asJsonSchemaTarget). MCP tools take ajsonSchemaTarget, andtools/listconverts tool schemas to draft-07 for clients on protocol revisions before 2025-11-25.zod()takesflavor: "mini"(typed asZodFlavor) to generate Zod Mini schemas, and CloudEvents take adataschemaoption that receives aDataschemaContext. -
New
better-supabase/overlaysubpath for OpenAPI Overlay 1.0, 1.1 and 1.2 documents.applyOverlayandapplyOverlaysapply them to any JSON document, with RFC 9535 JSONPath targets, a diagnostic for each target that matches nothing, and the 1.2 reusable actions (expandReusableActions).defineOverlaytypes an overlay in code, andoverlayFromDiffwrites the overlay that turns one document into another. JSONPath comes from the optionaljsonpath-rfc9535peer, loaded on first use, or from an engine you pass. The subpath imports no Node built-in, so it runs on every WinterTC runtime. -
A cleanup of shared code inside the package, with a few fixes.
-
Fix
doctorBS303 comparing generated files with a different line-ending rule thangen, fixsql upgradeignoring the provider defaults for modules, and flush pending events infinallyin the React Router, SvelteKit, Hono, Edge and oRPC adapters when a handler throws. -
Bound the HS256 key cache and the verified-token secret ids, and strip the event type prefix the same way in every block.
-
Move the Standard Webhooks code to
core/signaturesand the HMAC helpers tocore/hmac, share one optional-peer loader and one bounded TTL cache, and addmethodNotAllowed()tocore/problem. None of these paths are public exports. -
Split
config/config.tsintoconfig/types/*,expose.ts,json.ts,resolve.tsanddefaults.ts.resolveConfignow resolvesgen.watchInterval(default 2000), the SQL registry and the CLI read their fallbacks fromCONFIG_DEFAULTS, andGenTask,IntegrationNameandSkillsAgentderive from the newGEN_TASKS,INTEGRATION_NAMESandSKILLS_AGENTStuples, whichbetter-supabase/confignow exports. -
Split
sql/registry.tsintomodule-types.ts,registry.ts,module-context.ts,render.ts,paths.ts,markers.ts,upgrade.ts,introspect.tsand one file per inline module undersql/modules. A module declaresextrasfor SQL that depends on the whole layout, replacing the name switch in the renderer, and the tenant and provider entitlement checks share one template. -
Share one cron route guard (
core/cron-route), one time budget (budget), one lease heartbeat, oneblockEmitterand onetoBlockErroracross the jobs, outbox, webhooks, notifications, organizations and billing blocks. -
Split the jobs, billing, notifications, data-lifecycle and webhooks blocks into type, helper and per-factory files; the public subpaths are unchanged.
-
Split
server/resource,server/server,core/repository,core/define,next/create,testing/conformance,ai-sdk/mcp,ai-sdk/messagesandworkflow-sdk/worldinto type, helper and per-kit files, and share oneisRecordfromcore/guards. -
Share the AI SDK HTTP helpers, add
withFlushto flush events after a response, and run the React hooks on the same bindings as Vue, Solid and Svelte (ClaimsOfandProfileOfnow live in the bindings). -
Build the MCP table tool schemas from the same page schemas as the OpenAPI document, so list pages describe their page counts, and share
integerandwhereClausebetween the Postgres and SQLite compilers. -
Split server client construction into
server/clients.ts(with an injectablecreateClient),db.$searchintocore/db-search.ts, the topic SQL intorealtime/topic-sql.tsand the page helpers intocore/paginate.ts. The incoming webhook endpoint now shareshitRateLimitandreceiveWebhookwith the rate limiter and the inbox, andgenbuilds the spec step withspecOptionsFromConfig.
-
-
powersyncExecutortakes afallbackexecutor. Reads that SQLite can't run (an include, full-text search, a function source) anddb.$rpcgo to the fallback, usuallypostgrestExecutor(supabase); writes never fall back.checkSqlite(betterSupabase, list, { all: true })returns every unsupported feature a list uses instead of the first.db.$rpcon an executor withoutrpcnow returns aDbErrorof kindunsupported(HTTP 501) instead ofinvalid_request. The PowerSync docs add a guide to porting a PostgREST list. -
New
better-supabase spec emit,spec checkandspec validatecommands.spec emitrenders every output in the newspecsconfig key (format, version, output file, overlays, JSON or YAML, and an optional reference page), applies the overlays and caches renders by content hash.spec checkfails when a file on disk is out of date,spec validatechecks each document against the official JSON Schema for its version, and every run writes a diagnostics manifest.--fail-on,--ui,--watchand--manifestcover CI, reference pages and local work.openapi emit --checknow compares text the same wayspec checkdoes.better-supabase scaffold apiwrites the resource routes and an OpenAPI route for Hono or Next.js:lib/<name>.generated.tson every run andlib/<name>.tsonce, for your own changes.The
openapiconfig key is deprecated. It still works and maps ontospecs; move it tospecs.outputswithformat: "openapi". -
New
better-supabase/specsubpath:defineApibuilds one API model from the schema, resources, routes and plugindescribehooks, and renders it in any registered document format and version, with transforms, overlays and lint diagnostics.defineDocumentFormatadds a format of your own (apiVersion: 1),serializeDocumentandspecResponseserve a rendered document with a strong ETag, andspecReferenceserves a reference page for Scalar, Swagger UI, Redoc, Stoplight Elements or RapiDoc, from jsDelivr at pinned versions with SRI hashes or from your own assets, with a CSP nonce.better-supabase/openapinow renders OpenAPI 3.0, 3.1 and 3.2 from that model, and3.3-previewwhen you ask for it.createOpenApikeeps its signature and takes aversionoption. It now emits OpenAPI 3.1.2 by default (it emitted 3.1.1), so a snapshot of theopenapifield changes. 3.0 output lowers schemas to the 3.0 Schema Object, and the 3.2 fields travel asx-oai-*extensions below 3.2.openapiFormatlints a document for duplicate operation ids, path parameters that don't match the template, unknown security schemes and$refs that don't resolve. The tenant, soft delete and timestamps plugins describe their columns and headers in the document. -
Add the
standardSchema()generator. It writes<output>.standard.tswith a Standard Schema per table for its Row, Insert and Update shapes, built on the newtableSchema()runtime helper, so apps can validate writes without installing zod or valibot. Each schema also implements Standard JSON Schema for thedraft-2020-12,draft-07andopenapi-3.0targets, and the file exportsvalidatorsfor the validation plugin.gen --metadata --emit standard-schemaprints it. Generators now receiveruntimeImportin their input. -
bucket.connect()andbuckets.connectStored()take anyStorageApi, not only a supabase-js client:StorageClientis nowStorageApi | { storage: StorageApi }, whereStorageApiis an object whosefrom(bucket)returns aStorageBucketApi, the file methods ofsupabase.storage.from(bucket)typed structurally with the supabase-js{ data, error }results. A supabase-js client and itsstoragestill type-check unchanged.bucket.apply(),defineAnalyticsBucket().connect()anddefineVectorBucket().connect()call the admin API, so they take the newStorageAdminClient(a supabase-js client) instead. -
Supabase Lite support.
createServer(betterSupabase, { backend: "lite" })verifies Lite's HS256 tokens locally againstSUPABASE_JWT_SECRET(a newenvsetting, at least 32 characters), skips the JWKS prefetch, and withliteDriverset tosqlite-postgresorsqlitereturns anunsupportederror for$rpcwithout sending a request.liteStack()inbetter-supabase/testingruns Lite in the test process on in-memory SQLite or PGlite, applies migrations, schemas and seed SQL, and signsasUsertokens with the Lite secret.genanddoctorread a Lite project's[db] driver: thepostgresdriver's[db] url, or the migrations and declarative schemas replayed into an in-memory PGlite (source.lite: falseturns this off). Doctor adds BS328 ($rpcon a Lite SQLite driver) and BS329 (Realtime or Edge Functions on Lite), and skips BS403 on Lite projects.init --liteprints Lite's install and CLI steps.@supabase/liteis an optional peer (>=0.11.0 <0.12). -
Connect agents to Supabase's MCP server and serve it from your app.
supabaseMcp()inbetter-supabase/ai-sdk/mcpreturns the server URL for a project and the tool schemas from@supabase/mcp-server-supabase, andconnectToolstakes them asschemasto type each tool's input and output.supabaseMcpHandler()inbetter-supabase/mcp/sdkserves Supabase's MCP server behindcreateMcpAuth: it checks the caller withauthorize, reads the Management API token through aCredentialProviderand stays read-only unless you passreadOnly: false.@supabase/mcp-server-supabaseis an optional peer (>=0.13.0 <0.14) loaded on first use. -
Support sessions that pass their expiry are now ended and recorded. The
support-sessionsSQL module addsexpire_support_sessions(), for the service role only, which ends every running session pastexpires_atthroughend_support_session(id, 'expired')(so each gets itssupport.endedaudit entry) and returns them.bs.support.expire()runs it through the store's new optionalexpire()(the SQL store calls it overpostgres.admin) and emitssupport.endedwithendedBy: "expired"for each session, andbs.support.expireRoute({ secret })serves it to a cron caller. A store withoutexpire()fails withSUPPORT_EXPIRE_UNSUPPORTED, andtestSupportSessionStorechecks thatexpire()leaves running sessions alone. -
supportStorage()andctx.storagereturn aStorageApi, the file API ofsupabase.storage, sobucket.connect(ctx.storage)and blocks that takestoragework in a support session.listandlistV2callstorage.searchandstorage.search_v2over Postgres as the target, so RLS filters each page as Storage would: a page is no longer shorter thanlimitwhen some entries are hidden (which endedbucket.listandsweepearly), and the prefix matches without case, as Storage matches it.existsandinforeadstorage.objectsas the target, anddownload,createSignedUrlandcreateSignedUrlscheck visibility there before the service client serves the object, now withtransform,versionId,cacheNonceanddownloadpassed through. A hidden object answers like a missing one (NoSuchKey), and paths are normalised as storage-js normalises them before the check.getPublicUrlis built without a check. Every write resolves to a403error withSUPPORT_READ_ONLYorSUPPORT_STORAGE_WRITES, which a connected bucket maps toforbidden. TheSupportStorage,SupportStorageAdmin,SupportStorageBucket,SupportStorageEntryandSupportStorageListOptionstypes are removed. -
In a support session,
ctx.storagereads Storage as the target user, wherectx.supabasethrows.supportStorage({ admin, sql, readOnly })is exported frombetter-supabase/serverfor the same reads outside the server context. -
better-supabase/tanstack-db/supabaseaddssupabaseCollection(betterSupabase, supabase, table, options), a TanStack DB collection on@supabase-labs/tanstack-db(a new optional peer,>=0.1.0 <0.2). Live queries load on demand through PostgREST,realtime: truekeeps them current throughpostgres_changes, and inserts, updates and deletes go through the table's repository, so RLS, hooks, events and plugins apply.writes: "direct"keeps the package's own handlers. It needscasing: "snake"and tables in thepublicschema;collectionOptions()frombetter-supabase/tanstack-dbstays the choice for camel casing. -
Call Edge Functions with a typed input and output.
defineFunction()inbetter-supabase/edgechecks the JSON body with a Standard Schema before the handler runs as the caller (422 with the issues when it fails) and can check the output too.ContractOf<typeof fn>exports its contract, andfunctions<Contracts>(supabase).invoke(name, input)inbetter-supabase/clientcalls it through supabase-js'sfunctionsclient and returns anAsyncResult: Problem Details come back as theirDbError, and network and relay failures asnetworkerrors. -
attachments.uploadandattachments.putdelete the record they created when Storage refuses the signed upload URL or the upload, such as a bucket policy or the read-only Storage of a support session, and return the refusal. Before, the record stayedpendingwith no object behind it. -
The CLI reads
$SUPABASE_DB_URLwhen$DATABASE_URLis unset, sogen,introspect,doctorandseed --applyconnect with the variable the Supabase CLI andbetter-supabase envwrite.$DATABASE_URLstill wins when both are set. Thegendocs now list the real source order: a configuredsource.snapshotcomes beforesource.dbUrl. -
bs.loader()without a request now namescreateStaticLoaderfromexpo-serverand the SDK 57unstable_useServerRenderingoption. The Expo docs say whichapp.jsonoptions SDK 57 needs and that SDK 58 needs none, and show how to callbs.requestinsidecreateServerLoaderor wrapbs.loader, which is anexpo-serverLoaderFunction. -
Bind
jsonandjsonbarguments ofrpc()correctly over direct Postgres whendatabase.types.tsis absent: argument types now come from the catalog, so string arguments are JSON-encoded like they are with generated types. -
bs.authed()inbetter-supabase/orpcnow checksroles(and the newpermissionoption) on their own. Before, it checked them only whenrequireTenantorauthorizewas also set, so a procedure guarded byrolesalone let every signed-in caller through. -
The
@supabase/postgrest-typegenpeer accepts>=0.4.0 <0.5instead of exactly 0.4.0, so an app that installs another 0.4 release no longer fails strict peer checks. The CLI is still tested against 0.4.0, andgenprints a notice when another release is installed. Doctor's new BS601 reports an installed peer outside the range better-supabase declares, and the new Peers page lists every peer, its range and the subpaths that need it. -
The Postgres executor (
better-supabase/postgres) now sends values forjsonandjsonbcolumns as JSON text in inserts, updates and upserts. Arrays, arrays of objects and plain strings failed before withinvalid input syntax for type json, because node-postgres sends a JavaScript array as a Postgres array literal. A string is stored as a JSON string, as it is over PostgREST,nullstays SQLnull, and each element of ajsonb[]column is encoded the same way. Postgres array columns such astext[]still receive arrays. Comparison filters (eq,neq,gt,gte,lt,lte) on ajsoncolumn send an array or object operand as JSON text too. -
Postgres error
25006(a write in a read-only transaction, such as in a read-only support session) maps toforbiddenwith code25006, the message "This connection is read-only, so the write was refused" and a hint, instead ofunexpectedwith the raw "cannot execute ... in a read-only transaction" text. The original text is kept indetails. -
Queries through
ctx.dbandserver.dbForwith credentials that did not verify now return the unauthorizedDbErrorinstead of running as anon, so a handler without a route guard cannot read data with a bad token. -
The README now names
@supabase/postgrest-typegennext to the runtime packages, and links the new Supabase packages page, which lists every Supabase package better-supabase uses, how it is installed and how the CLI relates to thesupabaseCLI. -
The
support-sessionsmodule's platform-target check honourssql.modules.invitations.options.platformRoles.through.whereunder theprovideraccess model: a target counts as platform staff only through a role that matches the condition, so a roles table that mixes tenant and platform scopes no longer blocks support sessions for users who hold only tenant roles. -
The support session store reads its time values with the definition's
temporal:createServerpasses it tosupportSessions(...).createand on to the store through the new optionalSupportSessionStore.withTemporal, andsqlSupportStore(postgres, { schema, temporal })takes an explicit namespace that wins over it, as block creators do. -
bucket.sweep({ olderThan })recognizes aTemporal.Instantfrom any copy of Temporal by its tag, so an instant from a second polyfill copy is no longer read as a duration.createJobs(source, queues, { now })takes a clock forrunAtdelays and schedule runs, and block events onbetterSupabase.eventstake theirtimefrom the definition'snowoption. The Temporal docs now lead withdefineSupabase(schema, { temporal })and keeptemporal-polyfill/globalas the alternative. -
New subpath
better-supabase/better-result:withBetterResult(db, toResult)returns a view ofdbwhose repository methods,$rpc,$run,$many,$searchand list queries ($list) return better-resultResultvalues, so call sites no longer convert each result.toResultis adefineBetterResultErrorsconverter or{ Result, mapError }. better-result is an optional peer (>=3 <4) that the subpath imports only for types.
0.7.1 (2026-10-11)
suspendAccountwithsuspended: truebans the user and keeps their sessions, so Auth answers refresh andGET /auth/v1/userwithuser_banned,sessionStatusreturns"banned"andbs.proxy({ endedSession })redirects withreason=account_banned. Deleting the sessions made Auth answersession_not_found, so a suspended user read as"ended". ThesessionsSQL module'ssession_active()already refuses a banned user's tokens.suspended: falsenow ends the sessions kept during the ban before it lifts it, so a reinstated user signs in again, andendedis0on a suspension. PassendSessions: trueto delete the sessions on suspension too, for apps without thesessionsmodule. Doctor's BS106 description and the docs now say thatfromPoliciesmakes only tables with RLS on service-only.
0.7.0 (2026-10-10)
sessionStatusreturnsbannedwhen Auth rejects a token witherror_code: "user_banned", a rejectedrefreshSessionoutcome carries the Authcode, and the resolution reportssessionEndedReason.bs.proxy({ endedSession })redirects banned accounts withreason=account_banned;endedSession.reasonsrenames thereasonvalues.SessionStatusgains"banned", so a switch over it needs a case for the new value.better-supabase/serverexportstoCsv,csvColumnsandcsvResponse, which answers with a CSV attachment. ThesessionsSQL module addsbetter_supabase.last_sign_ins(uuid[]), callable by the service role only, andbetter-supabase/serverexportslastSignInsto read many users' last sign-in in one query. Runbetter-supabase sql syncto write the function.- With
sql.modules.grants.options.fromPolicies, a table the schema files create that has no permissive policy and noexposeentry is now service-only:sql syncrevokespublic,anonandauthenticatedon it and grantsservice_roleevery privilege. BS106 no longer asks for aselectgrant on those tables. New doctor check BS115 reports such tables that still hold grants on the live database, and BS116 reportssecurity definerfunctions in an exposed schema thatanonorauthenticatedcan execute while noexposeentry or policy uses them. - Doctor's BS304 now renders the SQL module files with the same layout as
sql sync(grants from policies, session policies, audited tables and the vector schema), so the two no longer disagree about stale files. BS313 skips projects that setsql.modules["rate-limit"].options.preRequesttofalse.
0.6.1 (2026-10-10)
-
The attachment scanner's
sink()andjobno longer fail for an attachment that was deleted before its scan. A missing attachment made the sink throw, so the outbox relay retried the whole batch until it dead-lettered it and every later event for that consumer waited behind it. The sink now skips that event and the job completes; download, transport andscanerrors still throw and retry. The object scanner does the same for an object deleted from storage (NoSuchKey), while a missing bucket still throws.scanner.scan()andobjects.scan()keep returning thenot_founderror. -
Audit entries record the request and correlation ids of the action that made them, so an app can group an event with the row changes of the same request.
createServergives every context a request id (the incomingx-request-idwhen valid, else a new UUID) and a correlation id (the incomingx-correlation-id, else the request id), exposes them asctx.requestIdandctx.correlationId, sends them as headers onctx.dbrequests and sets them as the transaction-localbetter_supabase.request_idandbetter_supabase.correlation_idsettings forctx.sql. TherequestIdsserver option renames the headers, ignores the incoming ones or turns the ids off, andContextOptionstakesrequestIdandcorrelationIdfor jobs.The audit module (version 6) reads the ids from those settings, then from the request headers (
requestIdHeaderandcorrelationIdHeaderoptions), keeps only ids of 1 to 128 safe characters throughbetter_supabase.request_id_or_null, and indexescorrelation_idon a managed log. The ids are metadata and never grant access. Runbetter-supabase sql syncand generate a migration to pick up the module change. -
The data exporter's
sink()andjobno longer fail for an export they can't claim, such as one cancelled, deleted or already finished before itsdata_export.requestedevent arrived. The sink threwDATA_EXPORT_NOT_FOUND, so the outbox relay retried the batch until it dead-lettered it and held every later event for that consumer behind it. The sink now skips that event and the job completes; a failing export still throws and retries.exporter.run()keeps returning thenot_founderror.
0.6.0 (2026-10-09)
-
The
agents,connectorsandai-tasksSQL modules store assistants, their MCP servers and scheduled prompts, andbetter-supabase/everuns eve agents on Supabase.createAgentsinbetter-supabase/blocks/agentsstores agents with instructions, model, tools, connectors and knowledge scopes, private until published.createConnectorsin/blocks/connectorsstores an organization's MCP servers, a grant per user behind acredential_ref, and tool lists an admin approves.createAiTasksin/blocks/ai-tasksruns prompts on a cron.better-supabase/ai-sdk/agentsaddscreateAgentRuntimeandmoderationMiddleware, and/ai-sdk/mcpaddsauthorizeConnector,connectToolsandconnectAll(@ai-sdk/mcpis an optional peer).better-supabase/eve(Node only,eveis an optional peer) addssupabaseAuth,credentialAuth,supabaseMemory,persistSessionsandrouteInbox.- Only the service role sets a task's
next_run_at, a task's chat must be its user's and its agent the user's own or published, anddrain()runs every claimed task before it reports the first error.
-
The
ai-chatandai-filesSQL modules store AI conversations and their files, andbetter-supabase/ai-sdkconnects them to the AI SDK (ai,@ai-sdk/reactand@ai-sdk/workfloware optional peers).createAiChatinbetter-supabase/blocks/ai-chatkeeps chats, a branching message tree, runs, tool approvals, feedback, share links, a model catalog per plan and moderation events;/blocks/ai-chat/reactaddsuseAiChats,useAiChatTree,useAiModelsanduseAiShare. Messages use a canonical format (aiMessageSchema,schemas/ai-message-v1.json,SPEC_PINS.aiMessage).createAiFilesinbetter-supabase/blocks/ai-filesstores uploads in two steps with policies that follow the file row, and versioned documents./ai-sdk/filesaddsaiFileDownload,saveGeneratedFilesandproviderFile.better-supabase/ai-sdkadds message converters, AI Gateway helpers and job handlers,createAssistantin/ai-sdk/chatserves a resumable chat route, anduseAssistantin/ai-sdk/reactisuseChatwired to it./ai-sdk/workflowruns answers as Workflow SDK workflows withdurableChatanddurableTurn, anduseDurableAssistantis the client half.createAiChat(...).runsreads run steps, andcreateHarnessSessionsreads harness sessions.- Harness sessions are read on the server only, the idle-stop job skips a sandbox whose session a turn holds, and the chat list hooks keep every loaded page on a live update and ignore stale responses.
-
The AI blocks share one run-state vocabulary, one sandbox table and one set of permission keys. The ai-chat, ai-tasks, ai-providers and memory modules move to version 2;
better-supabase sql upgradebrings a database installed from an earlier build forward.- Runs end
completed,failedorcancelledin ai-chat, ai-tasks and workflows.RunState,FinalRunStateandFINAL_RUN_STATESfrombetter-supabase/blocksname them. createAiChat(...).runshas the run lookups, and durable chats read them from the context'schats.- ai-chat owns
ai_sandboxes. Harness sessions keep their sandbox there, andcreateAiChat(...).sandboxes.idleStopJobstops chat and harness sandboxes with one claim. Set the idle time withsql.modules.ai-chat.options.sandboxIdleAfter. - ai-files, knowledge, memory, agents, connectors, ai-tasks and ai-providers check
ai.*permission keys. ai-chat keeps itsai_chat.*keys, and the default roles grant both. MemoryScopegainsproject, withprojectIdon namespaces and memory rows.- eve sessions release runs with the new statuses, and the jobs page documents jobs cron, workflow schedules and ai-tasks as three scheduling layers.
- The eve, memory, ai-files, workflow-builder and ai-providers blocks keep their row decoders in
rows.ts.
- Runs end
-
The
ai-cacheandai-providersSQL modules cache model responses and keep each organization's provider keys, andbetter-supabase/ai-sdkmeters model calls.createAiCacheinbetter-supabase/blocks/ai-cacheandcacheMiddlewarein/ai-sdk/cachereturn a stored response or stream for a repeated call, per tenant with a TTL.createAiProvidersinbetter-supabase/blocks/ai-providerskeeps provider keys ascredential_refrows with batch and sandbox registries.byokOptionsandtenantGatewayOptionsturn them into AI Gateway BYOK options,trackedSandboxrecords sandboxes, andaiBatchesin/ai-sdk/batchesruns provider batches from a job.meterTelemetryrecords each call's tokens and cost on the tenant's usage meters, andspendReconciliationcompares them with the AI Gateway spend report.
-
The API keys block (
better-supabase/blocks/api-keys) issues keys with a checksum, reports their state and authenticates REST routes.- Keys end in a CRC-32 checksum, so
verifyand secret scanners reject a mistyped key without a lookup (parseApiKey). - Keys report
state(active,grace,revoked,expired) andsuccessorId(ApiKeyState). withApiKey({ keys })is a pipeline entry that verifies the key and contributesctx.auth, andapiKeyClaimsandapiKeyResolvertake aclaimshape so the authorization provider reads the key's scopes.options.scopes: "catalog"limits scopes to the provider's permission keys, and the provider model refuses the*scope unless listed.
- Keys end in a CRC-32 checksum, so
-
Every SQL module audits its security-relevant actions. Run
better-supabase sql sync.ctx.recordtakes a requiredaudit: one of the shared categories inAUDIT_CATEGORIES(membership,access,security,configuration,billing,data,ai,integration), orfalsefor content and status events. With theauditmodule installed, each action writes one audit entry in the same transaction as its outbox event.- Settings, flags, billing customers, credentials, connectors, agents, AI provider keys, tool policies and approvals, chat shares, incoming webhooks, webhook secrets, announcements, published workflows, API keys, invitations, the waitlist, SSO domains and SCIM changes now write an audit entry and an outbox event. The access catalog's tables are audited through the row trigger under
access. organizations,organizations-suspensionandsupport-sessionsrecord through the same path: organization entries move fromorganizationtoconfigurationormembership, support entries fromsupporttosecurity, and revealed audit details fromaudittosecurity. An adopted log with a category check maps the names withsql.modules.audit.options.values.category.options.auditCategoryis deprecated and still overrides every category of its module.sql.modules.<name>.audit: falsekeeps a module's actions out of the log; they still reach the outbox.support.startedrecords the session's tenant, likesupport.ended.
-
audit.sink()files events under the categories the SQL modules use:account.*andsupport.*events getsecurity, and acategoryoption maps any other event type to one ofAUDIT_CATEGORIES. -
createAuditLog({ transport })inbetter-supabase/blocks/auditlists, reveals and exports the audit log as the caller, and entries record who acted, from where and on what. Runbetter-supabase sql upgrade.list(list_audit_events) filters by tenant, event type, actor, target, record, category, outcome, source and correlation id, withsearch, a cursor oroffset, andcount: true.reveal(entryId)returns restricted details and recordsaudit.revealed.export({ format: "ndjson" | "csv" | "ocsf" })streams the log (CSV takescolumns,preambleandformatRow),exportToStoragewrites it to a bucket, andrecord(event)callsaudit_event.- Entries record actor kind and label, tenant and target labels,
summary,request_id,correlation_idandscope. Actor and request details are honoured only from the service role and admin connections;audit_event_trustedserves an app'ssecurity definerfunctions andoptions.trustedRoles. better_supabase.audit(...)keeps a table's settings on its trigger, so schema files need no data rows, andbs_audit_forget_droppedclears registrations of dropped tables. Adopted logs map their own values withoptions.values,tenantLabelandmetadataColumns.sql addwrites a pgTAP file per audited table. Doctor BS315 reports tables without an audit trigger and BS322 registrations of missing tables.
-
Authorization libraries plug in through a versioned
AuthorizationProvider(API v1, frombetter-supabase/config) in theauthorizationconfig key, and the access module'sprovidermodel renders the provider's SQL templates.- Under the
providermodel, the provider'sidsWithFor,isPlatformForandcanAssignFortemplates letcan_user()andmember_can()answer for any user, the invitations module check the inviter again at accept, and the notifications module filter recipients by their read permission.sql.modules.access.functions.canAssignFordefinescan_assign_asunder the provider and custom models. sql.modules.access.disableddefaults to the provider'ssuspensionrows.disabled.tenantanddisabled.useralso take an active-row shape ({ table, id, disabledAt, status, active }), where a missing row counts as disabled.testAuthorizationProviderfrombetter-supabase/testingchecks a provider, andtemplateFunctionsfrombetter-supabase/sqllists the functions a template calls. Doctor BS214 compares a Storage or Realtime policy's scope with the provider'spermissions[].scopesand reports keys not markedsqlComplete: true.- The CLI rejects a provider with an unknown scope or a parent cycle, a tenant scope without an
idType(SCOPE_ID_TYPES), adecidingColumnsentry that isn'tschema.table.column, or anotherapiVersion(providerApiProblem).testAuthorizationProvideralso checks thatrequireslists every function a template calls and that the token hook claims don't overlap. - Without
canAssign, only the service role assigns roles under theprovidermodel. Doctor BS411 names each installed module missing a template it needs. functions.permissionsForfillsmember_permissionsandpermission_claims, andfunctions.canApprovewithapprovals.distinctApproverdecides AI tool approvals. Bucket and topic policies takesql: "provider"to use the provider's templates,createAgentRuntimetakes atoolApprovalhook, the claims config has amembershipspath (claimPaths), and theapiKeycaller carriescreatedAtandcreatedBy.- Breaking: every PermDock-specific setting and export is removed.
sql.modules.access.model: "permdock"becomes"provider",entitlements.permdockbecomesentitlements.memberships, bucket and topicpermdockpolicies becomeaccesspolicies withscopeandsqltemplates,permdockVerifierand thepermdockclaim option ofapiKeyResolverbecome the provider package's job and the neutralclaimoption, and doctor reads the provider instead ofpermdock.config.ts, the manifest and the catalog. The 0.5 to 0.6 guide has the old-to-new table.
- Under the
-
The billing block (
better-supabase/blocks/billing) runs a tenant's Stripe subscription over the Stripe Sync Engine and gives platform staff typed lists across tenants.changePlan,cancelAtPeriodEnd,invoices,paymentMethods,voidInvoice,customerDetails,updateCustomerand tax ids (StripeTaxId).options.planspoints at your plan catalog with pricevariants, andcheckoutmergesparamsdeeply.- Staff with
viewAllreadallSubscriptions,allInvoicesandallCustomers, also asbilling_platform_*SQL functions. StripeSourceaccepts a function that returns a client, andensureCustomeris idempotent per organization.
-
Blocks can be extended without forking them: add your own columns and type them, steer methods with hooks, wrap the transport and add methods.
options.extraColumnson theorganizationsandprofilesSQL modules adds your own columns to the managed table, copies them on writes and returns them from reads.list_my_organizationsreturns the attribute and extra columns in a newattributes jsonbcolumn.- A Standard Schema
fieldsoption oncreateOrganizationsandcreateProfilestypes those columns, parses them on read and turns a bad write into avalidationerror.createNotificationsparses storeddatawith its type's schema onget,listandpage, soitem.datanarrows onitem.type(NotificationOf); data the schema rejects fails with the hintNOTIFICATION_DATA_INVALID. - A
hooksoption on those blocks, and oncreateBlockskeyed by block name, runsbeforehooks that refuse a call with aDbErroror replace its arguments, andafterhooks that observe a copy of the result.withBlockHooksdoes the same for any block. - New SQL hooks:
before_organization_update,after_organization_update,before_profile_update,after_profile_updateandbefore_notification_send. wrapTransportruns versionedBlockTransportMiddlewarearound every block call, checked bytestBlockTransportMiddlewareinbetter-supabase/testing, andextendBlockadds methods and refuses to redefine one.
-
createBlocksin the newbetter-supabase/blockssubpath builds several blocks from one set of options, and the blocks share their helpers, option names and page shape.createBlocks({ transport, service, schema, credentials, events, audit }, factories)builds the blocks you pass and wires their siblings: the AI files block into knowledge,credentialsinto connectors, ai-providers and the workflow builder, notifications into ai-tasks (withaiTaskNotification), and block events into an audit sink. The subpath also exportsrpcTransport,sqlTransportand theBlockOptionsandCursorPageOptionstypes.audit.sink()returns anEventSinkthat records each CloudEvent in the audit log, with its type, subject, tenant and data.createServer({ audit })sendsaccount.suspended,account.unsuspended,account.deletedandaccount.sessions_endedfromsuspendAccount,deleteAccountandendSessions, and forwardssupport.denied; a failing sink never changes a result.- Block lists page with
{ limit, cursor }, which replacesize,beforeandafter. - Notifications, organizations, profiles and outgoing webhooks take
mappersinstead oferrorMappers. CredentialProvidergains an optionalset(ref, value, { subject, description }), which Vault implements and the workflow builder uses.credentialRouter(providers)serves several providers as one, andrevokeIfConfigured(provider, ref, { subject, tenant })revokes only when a provider is configured, can revoke the ref and the ref is in the tenant.Embedder,EveDocumentBackend,AiTaskRunner,GraphCompilerandBuilderStartertake an optionalapiVersion: 1; a block refuses another version.better-supabase/testingaddstestEmbedder,testEveDocumentBackend,testAiTaskRunner,testGraphCompilerandtestBuilderStarter.- The block row decoders moved into the core and every block uses the same ones;
better-supabase/blocks/knowledgeno longer exportsvectorLiteral.
-
The CLI finds its config from any package in a workspace, pg-delta is the default diff engine, and doctor reports each problem once.
better-supabase.config.*andsupabase/config.tomlare found in the working directory or a parent up to.git, andinitat a workspace root asks for the package or takes--package <dir>.initturns on[experimental.pgdelta]unless it is set tofalse, and doctor BS316 warns projects still on migra.better-supabase configprints the resolved config.- Doctor skips findings the performance advisor already reports, runs splinter with the
[api] schemas, and adds BS222 (int8decoded asnumber), BS317 and BS318 (statements pg-delta can't order) and BS319 (SQL that alters a reserved role).tables.<name>.serviceRole: truemarks a server-only table for BS106 and BS107.
-
better-supabase/vue,/solidand/sveltebind the browser client with the features ofbetter-supabase/react, and new hooks and query helpers cover forms, uploads and optimistic updates.useActionanduseActionFormcall abs.action()and trackpending,data,errorandfieldErrors.better-supabase/reactaddsusePresence,useSignIn,useSignOut,useDebouncedSearch,useSignedUrlanduseUpload, andupload()takesonProgress.better-supabase/queryaddsoptimisticwith rollback,{ maxPages }for infinite queries andescapeLike, andcreateQueriestakes ascope.better-supabase/tanstack-dbaddscollectionOptions.clearOnUserChangealso resets queries when the token changes tenant, role or assurance level.bindClientreturnsdispose(), andliveQueryrejoins a failed channel with backoff.
-
genwrites tighter types from CHECK constraints, documents tables in the generated validators and can run from a metadata document. Runbetter-supabase gen.- A nullable column a CHECK requires is typed not null, and
tables.<table>.insertOptionalmarks columns the database fills on insert.relations: { nullableUnderRls: true }types to-one relations to RLS tables as nullable. zod(),valibot()andjsonSchema()add titles, descriptions and examples from comments, and enforce bounds from simple CHECK constraints.- The generated module exports
WhereOf<"table">,OrderByOf<"table">andOrderTermOf<"table">, and the metadata module is about 40% smaller. gen --metadata <path|->generates from aGeneratorMetadatadocument for@supabase/typegen, anddoctor --metadatachecks it.- Generators declare
apiVersion: 1and receivemodel.gendeletes files it no longer writes, fails on name collisions, andgen --watchreloads the config and retries. - Breaking: colliding
_by_relations are named by every key column (customerByCustomerOrganization) instead of ending in_, and view copies are no longer relations. Keep an old name withtables.<table>.relations. - Breaking:
@supabase/postgrest-typegenis an optional peer; install it forgen,introspectanddoctorwithpnpm add -D @supabase/postgrest-typegen@0.4.0. - Breaking: a
GeneratorInputbuilt by hand needs amodel.
- A nullable column a CHECK requires is typed not null, and
-
The comments and attachments blocks (
better-supabase/blocks/comments,/blocks/attachments) store threads and files on any subject with per-subject permissions.options.subjects.<type>takespermissions: { read, create, moderate },cascade, andlabel,pathandreadableByfor notifications; a mentioned member is notified only when they may read the subject.- Comments keep a rich-text
document(mentionsOf,options.documentSchema), expandoptions.mentionGroups, and havecopy(),history()andcounts(). - Attachments take a
bucket,allowedMimeTypesandtenant: falseper subject, keepmetadata, and haveput(attachment, file)andread(id). A malware scan gate works for any bucket (object_clean(bucket, path),createObjectScanner,options.scanBuckets).
-
Errors map to typed messages and result libraries, plugins behave the same whoever wrote them, and each definition keeps its own state.
createErrorMessages(messages)needs a message for everyDbErrorkind.toBetterResultreturns better-result's ownResult<T, E>when that is the expected type, anddefineBetterResultErrorsmaps kinds toTaggedErrorclasses.P0002maps tonot_found.- Plugins get
enforce: "first", a mutationintent,scopes, and acontexthook perconnect(), so two definitions in one process no longer share a tenant. Hooks receive the caller'ssignal, anddb.$withoutPlugins({ keep })keeps named plugins. defineSupabase(schema, { temporal })takes theTemporalnamespace without patchingglobalThis, anddiagnostics: truelogs a record without tokens or row values for every query.- Breaking:
DbErrorgains the kindsquota_exceeded(429),max_affected(400) andunsupported(501). An exhaustiveswitchoverDbError["kind"]needs the new cases.
-
better-supabase/credentialsresolves third-party tokens from acredential_ref, andbetter-supabase/streamsstores resumable output for chats, workflows and agents.vaultCredentials()keeps secrets in Vault through thecredentialsmodule and verifies inbound signed requests.createServertakes acredentialsprovider asctx.credentials,subjectFor(ctx)gives the subject,better-supabase/vercel-connectaddsvercelConnectCredentials(), andtestCredentialProviderchecks other providers.- The
streamsmodule keeps ordered chunks with idempotent appends and a cancel flag, used bypostgresStreamStore,teeToStore,writeToStoreandresumeFromStore.better-supabase/streams/redisaddsredisStreamStore(redisis an optional peer), andtestStreamStorechecks other stores. - A tenant's
credential_refcarriestenant(tenantCredentialRef,credentialRefInTenant): connectors, AI providers, the workflow builder and MCP refuse a ref from another tenant withCREDENTIAL_REF_FOREIGN, Vault stores tenant secrets astenant/<tenant>/<secret>, andvercelConnectCredentialsrefuses a tenant ref for the app subject.
-
The data-lifecycle block (
better-supabase/blocks/data-lifecycle) exports, anonymizes and purges a tenant's or a user's data across every module table.- Modules declare their tables, so exports and purges cover every installed module, and
options.autoTablesadds a schema's tenant tables. Exports leave out keys, token hashes and secrets. - The purge retries referenced tables, stops with
ORGANIZATION_PURGE_BLOCKEDwhen stuck, and deletes the tenant row last.createOrganizationPurger({ buckets })takes path prefixes (PurgeBucket). options.anonymizerules anonymize rows after a retention period (anonymizeDue()), andcreateDataExportertakesformat: "csv".
- Modules declare their tables, so exports and purges cover every installed module, and
-
Block events follow one contract. Run
better-supabase sql syncandbetter-supabase codemod 0.6.- Every type is
<entity>.<past_tense_verb>. Renamed:ai_chat.message.completedtoai_chat_message.completed,inbox.conversation.*toinbox_conversation.*,inbox.message.receivedtoinbox_message.received,incoming_webhook.rotatedtoincoming_webhook.token_rotated,workflow.alerttoworkflow_alert.triggered,workflow.run.*toworkflow_run.*anddata_export.readytodata_export.completed. BLOCK_EVENT_RENAMESfrombetter-supabase/eventsmaps theorg.*types toorganization.*, and the 0.6 codemod rewrites string literals that name a renamed type.BlockEventMaptypes every event a SQL module records. Each module declares its events inNAMES.events, andctx.recordthrows for an undeclared type, a subject that is not a kebab-case plural, a missing payload key or a retried event without an idempotency key.- Support events carry a camelCase payload with
organizationIdundersupport-sessions/<id>. Push, waitlist and audit subjects arepush-devices/,waitlist-entries/andaudit-entries/. SCIM events namescimUserIdorscimGroupId. notification.created,webhook.disabledand the workflow events carryorganizationId.notification.created,webhook.disabled,attachment.scanned,inbox_message.received,data_export.completed,data_export.failedandorganization.purgedhave idempotency keys.notifications.sink({ map })turns outbox events into notifications, keyed by the event id.
- Every type is
-
Adapters for more server frameworks, typed structurally, with the guards and actions of the Next.js adapter. They replace the
@supabase/serverframework adapters, which upstream removes on 2026-12-01.- New subpaths:
better-supabase/tanstack-start,/sveltekit,/react-router,/h3,/h3/v1,/elysia,/node(toExpress,toFastify,toKoa),/nestjs(@nestjs/commonis an optional peer),/astro,/nuxtand/solid-start.toHono,toEdge,toOrpcandtoExpobridge the existing adapters. - Each factory puts
db,bs,auth,tenantandsessionon the request, withbs.require()andbs.action(); every guard takesrolesandroleClaim. The edge getsbs.routes({ "GET /customers/:id": handler })with typedctx.params(RouteParams), oRPC getsbs.authed(), andcreateMcptakesrequiredRoles. handle(),extendServer,flushEventsandresolveTokenbuild an adapter on the shared request path, checked bytestAdapter.withServerTiming(),withDbStats()andtagCachecover timing headers, database budgets and tag caches.- Breaking:
createEdge'scorsoption runswithCorsfrom@supabase/middleware/cors. A preflight needsAccess-Control-Request-Method(otherOPTIONSrequests reach the handler), and an allow-list addsVary: Origin.corsConfig(options)returns the config.
- New subpaths:
-
The
inboxSQL module andbetter-supabase/blocks/inboxadd a shared support inbox per organization, andbetter-supabase/chat-sdkruns Chat SDK bots on it (chat>=4.41 <5is an optional peer).- The inbox stores contacts, conversations with assignment, teams, snoozing and bot handoff, messages and notes, read receipts, deliveries and attachments, with
purgeContactfor data subject requests.useInbox,useConversationanduseInboxWidgetin/blocks/inbox/reactstay current over private Realtime topics. createSupabaseStateis aStateAdapteron thechat-sdk-statemodule,inboxAdapteranswers in the in-app widget, andwebhook,inboundHandler,deliver,maintainandcreateChatInstallationsrecord Slack, WhatsApp, Messenger and SMS conversations.testChatStatechecks anyStateAdapter.
- The inbox stores contacts, conversations with assignment, teams, snoozing and bot handoff, messages and notes, read receipts, deliveries and attachments, with
-
Invitations can be edited, answered from an in-app inbox and sent for platform roles under the
providermodel. Runbetter-supabase sql upgrade.update_invitation(organizations.updateInvitation) changes an open invitation and emitsinvitation.updated; it refuses an expired one, so callresend_invitationfirst.my_invitations()lists the signed-in user's open invitations, andacceptInvitationByIdanddeclineInvitationByIdanswer them. Invitations carryextrafrom theinvitation_preview_extrahook,createdAt,organizationand aninviter.options.platformRolesnames the platform role table, withthrough,canAssignandcanAssignForchecked at invite and accept.through.whereis required when its table is alsoroleThrough's, andbs_role_scoperefuses other roles on direct writes (PLATFORM_ROLE_SCOPE).- Breaking: accepting or updating an expired invitation fails with the hint
INVITATION_EXPIREDinstead ofINVITATION_INVALID, which now means unknown, accepted, declined or revoked. - Breaking:
Invitationhas a requiredinviterfield (nullwithout the profiles module), and theinvitation_preview_extrahook also runs for invite, resend, update andmy_invitations.
-
Jobs run as the user who enqueued them and report queue health, and
better-supabase/blocks/jobsadds leases and rate limits. Runbetter-supabase sql upgrade.bs.forContext(job.context)returns repositories as the context's user and tenant, so RLS applies in the handler. It keeps a read-only support session read-only, andclaimsFor(userId, context)rebuilds hook claims.jobs.stats(),listDead()andretryDead()serve admin pages, andenqueuetakesdedupe: "waiting".- Under
scheduler: "drain", schedules record a tenant (listSchedules,unscheduleAll),ensureScheduleskeeps a named set in step,drainRoutetakesmonitorhooks, anddevDraindrains locally. withLease(sql, key, fn)holds one holder per key.hit_rate_limit(scope, key)andcreateRateLimit().check()limit any key, andrateLimited(decision)answers 429 withRetry-After.- Breaking:
begin_idempotentreturns aholder, andcomplete_idempotentandrelease_idempotent(createIdempotency().completeandrelease) take it and return false after another caller took an expired key over.
-
Kits are now blocks, and every feature module lives under
better-supabase/blocks/<name>. There are no aliases; the 0.5 to 0.6 guide lists every rename.- Breaking: subpaths move.
better-supabase/orgsisbetter-supabase/blocks/organizations, andbetter-supabase/jobs,/notificationsand/webhooksarebetter-supabase/blocks/jobs,/blocks/notificationsand/blocks/webhooks.createOutboxandoutboxCloudEventmove tobetter-supabase/blocks/outbox, andpurgeAuditLogtobetter-supabase/blocks/audit.hasEntitlement,EntitlementKey,entitlementMembersandENTITLEMENTS_UPDATEDmove tobetter-supabase/blocks/entitlements(out ofserver,next,ssr,reactandjobs), anduseNotificationsmoves frombetter-supabase/reacttobetter-supabase/blocks/notifications/react. - Breaking:
sql.kitand thekitskey merge intosql.modules, an object keyed by module name whose values are the module settings (a list of names still works). - Breaking:
Kit*names for features areBlock*(BlockEvent,onBlockEvent,forwardBlockEvents), andKit*names for SQL modules areModule*(ModulesConfig,ModuleConfig,renderModules,modulePermissionKeys).orgis spelled out:createOrganizations,Organizations,organizationLogoBucket, and theorganization.*event types replaceorg.*. - Breaking: in SQL,
member_org_ids,has_org_roleandorg_member_rolearemember_organization_ids,has_organization_roleandorganization_member_role, the organization functions take anorganizationparameter,better_supabase.kit_modulesisbetter_supabase.modules, and module files carry-- @bs-moduleand-- @bs-module-datamarkers. Runbetter-supabase sql sync, the schema diff andbetter-supabase sql datato move a database over.
list,storage,realtimeandeventskeep their subpaths. The list, storage and realtime pages move to/docs/platform, and the events page to/docs/standards/events. - Breaking: subpaths move.
-
The
knowledgeandmemorySQL modules give agents retrieval and long-term memory; both requirevector-search.createKnowledgeinbetter-supabase/blocks/knowledgestores documents scoped to an organization, agent, project, chat or user, chunks them with embeddings and atsvector, and searches with reciprocal rank fusion. Withjobsinstalled new documents are enqueued for embedding;ingest.filereads anai-filesupload.createMemoryinbetter-supabase/blocks/memorykeeps core memory files under/memories, archival facts and one embedding per chat message, plus versionedmemory.documents.better-supabase/ai-sdk/embeddingsaddsembedWith,supabaseEmbed,rerankWith,searchToolandtoSourceParts, and/ai-sdk/memoryaddsmemoryTool,anthropicMemory,recallTool,withMemoryand theextractMemoriesjob handler.- An item rewritten while it is being embedded stays pending,
set_memory_embeddingswrites a batch, bad vectors fail withEMBEDDING_INVALID, and knowledgedraintries each documentattemptstimes (3) before marking it failed.
-
better-supabase/mcp/sdkserves MCP servers built on the official SDK (@modelcontextprotocol/server2.3 or later, an optional peer), andcreateMcpworks on hosted Edge Functions.createMcpAuth(betterSupabase, { resource })verifies Supabase tokens locally as anOAuthTokenVerifier, serves the RFC 9728 metadata and answers 401 and 403 challenges.withBetterSupabaseMcp(server, auth)gives every tool callbackdb,authandbs.- On Edge Functions the
resourcecomes from the function slug. Both servers answer CORS preflights (allowedOrigins) and takerequiredRolesandwaitUntil.
-
Expo and React Native get a server adapter, a native client, local SQLite executors and push notifications.
better-supabase/expo:createExpo(betterSupabase)gives Expo Router loaders, API routes and+middleware.tsthe verified caller;better-supabase init expowrites it.better-supabase/client/native:createNativeClient(betterSupabase, supabase)binds repositories without@supabase/ssr,secureStorageandlargeSecureStoragekeep sessions in the keychain, andautoRefreshOnForeground,syncQueryWithApp,persistQueryCache,uploadFromUriandhandleAuthDeepLinkcover the app lifecycle.better-supabase/react/nativeaddsuseOAuth,useAuthDeepLinks,useProtectedRouteandAuthGate.better-supabase/powersync:powersyncExecutor(db)runs the same repositories on PowerSync's SQLite,createUploadConnectorreplays queued changes andsyncWithAuthclears data on a user change;/powersync/reactaddsuseWatch,useSyncStatusanduseConflicts.better-supabase/expo-sqliteaddsexpoSqliteExecutor(db).- The
pushmodule andbetter-supabase/blocks/pushstore device tokens and send through the Expo Push API (registerDevice,expoPush,expoPushChannel).
-
The Next.js adapter authorizes actions and Server Components, signs out ended sessions and scopes cache tags to a tenant.
bs.action()andbs.route()takerequireTenantandauthorize(session, input), andbs.require(options)refuses a Server Component caller withunauthorized(),forbidden()ornotFound().bs.proxy(request, { endedSession })asks Auth whether the session still exists and clears the cookies when it doesn't, andexpiredPrefetch: "render"renders a prefetch with an expired token signed out.bs.cacheTag,bs.cacheTagsandbs.cachedtake{ tenant }, so a mutation in one tenant no longer revalidates other tenants' reads, andbs.context({ tenant })takes the tenant from route params.- Mutations outside a Server Action expire tags with
{ expire: 0 }, so a read after a write in a route handler is fresh (nextCache({ revalidate })keeps"max"). useSessionChangerefreshes the session before an organization switch re-renders, andtenantOf(session)reads the active tenant.- Breaking: the
nextpeer range is>=16.3 <17, becausecreateNextawaitsio(). Upgrade Next.js first.
-
The notifications block reads more of the inbox, hydrates a page in one call and reports who received a send. Run
better-supabase sql upgrade; a custom-mode module implements the new signatures.get(id),page({ offset }),markUnread,subscriptions()andpreferences()are new, andlistfilters by subject type, search and read, resolved or dismissed state.hydrate(items)loads whatrenderneeds for a page, andinclude: ["actor"]adds each actor's profile.sendtakeswatchers: falseandexclude, andsubscribe({ ifAbsent: true })auto-follows without overriding a member's choice.- Breaking:
markRead,markUnread,dismissandresolvereturn{ count, items }instead of a number, and their SQL functions returnjsonb. - Breaking:
send()returns{ id, recipients }instead of the id, andonSentandnotification.createdget the recipients. It callssend_notification(jsonb);notify(jsonb)still returns the id.
-
Organizations can suspend members, route deletion through data-lifecycle and give platform staff admin rights, and profiles store an avatar path. Run
better-supabase sql upgrade.suspend_memberandresume_member(organizations.suspendMember,resumeMember) keep a member's role but drop its permissions through the memberships table'sdisabled_at; they refuse the caller, the last owner and higher members.transfer_ownershiprefuses suspended or disabled new owners.options.deleteMode: "lifecycle"sends deletion through data-lifecycle's grace period, and"none"writes no delete function.permissions.updatePlatform,deletePlatform,updateRolePlatformandremoveMemberPlatformlet platform staff manage any organization.options.assignmentGuard: "external"drops the module's role guard, which now checks only client writes.- Profiles get
avatarPath,usernameFromtakes several keys with a separator, andreadPolicy: { platform }lets staff read every profile.reserved-slugstakesslugs,minLengthandmaxLength.
-
The outbox (
better-supabase/blocks/outbox) hands consumers the raw rows and moves failing events aside. Runbetter-supabase sql upgrade.outbox.consume(consumer, handler)passesOutboxEventrows (OutboxHandler), andrelayRouteaccepts such a handler.- A failing consumer backs off (
maxBackoff), and aftermaxAttemptsfailures an event moves tooutbox.deadLetters(consumer).purgetakes{ ignoreIdle }, andcreateOutboxtakes aBlockTransport.
-
Realtime topics support presence, write their own policies and build triggers from parent rows.
defineTopictakespresence, and subscriptions gettrack,untrack,members()andonPresence.realtime.policies: { from, output }writes every topic'srealtime.messagespolicies onsql sync, with a--checkdrift test.topic.triggerSql()takes lookups through parent rows, aneventname and a custompayload(TriggerLookup).realtime.usersgives live queries on a table a per-user topic, anduseBroadcastworks with a plain supabase-jsclient.
-
Breaking: 0.6 removes the deprecated aliases 0.5 kept for one minor. A support token whose
acthassession_idbut nokindis refused asinvalid-chain. The audit module (version 5) drops the read-onlybetter_supabase.audit_logview; readbetter_supabase.audit_events.maxUrlLengthis gone fromdefineSupabaseandpostgrestExecutor(useurlLengthLimit),scopesfrom the MCP server options (useadvertisedScopes), andcreateInboxand theInbox*types frombetter-supabase/blocks/jobs(usecreateWebhookInboxandWebhookInbox*).better-supabase codemod 0.6now renames theKitandOrgexports andmaxUrlLength, and lists imports from the moved subpaths for review. -
Repositories read with more filters and sorts, split long
inlists instead of failing, and count pages cheaply by default.findOnly({ where })returns the one matching row,nullfor none andmultiple_rows(409) for more.wheretakes jsonpathfilters (JsonPathOps),matchandimatchregular expressions, and json containment with arrays. Reads longer thanurlLengthLimit(6000) split along their longestinlist.orderBysorts by a to-one relation's column,aggregate()sorts groups by_countor a measure (AggregateOrderBy), andpaginate({ offset, limit })returns an offset window.defineReadSettakesauth.uid.- Breaking: list queries count with
count: "planned"by default; passcount: "exact"for the old behaviour. - Breaking: cursors record the table and sort, so cursors made before 0.6 and arrays from
encodeCursor()fail withInvalid cursor. Start again withafter: null.
-
Writes take conditions, return rows from bulk calls and cap how many rows they change.
update(key, patch, { where })returnsnot_foundfor a row that doesn't match,expecttakeswhereoperators, andupdateManyanddeleteManytakereturning: true.updateManyanddeleteManytakemaxAffected; a write matching more rows fails withmax_affectedand changes nothing. It needs PostgREST 13 (setpostgrestVersion: "12.2"on older servers), and the rules plugin'sstrict()preset requires it.- Every call takes
timeoutandretry,createManyandupsertManytakedefaultToNull, and mutations takecount: "planned". - Breaking:
deleteManyandupdateManyrefuse awherethat filters nothing. PassallowAll: truetoupdateManyto update every row. - Breaking: an update whose
datasets no columns returnsinvalid_requestinstead ofnot_found. - Breaking: a
*inlikeandilikepatterns is a literal character; use%.
-
db.$rpcis typed per overload, acceptsnullarguments and runs over the Postgres executor. Runbetter-supabase gen, thenbetter-supabase codemod 0.6, which lists every$rpccall to review.genkeeps every overload of a function as a union inFunctions, and$rpcpicks the overload by the argument names the call passes. Arguments are typedT | null.postgresExecutorimplementsrpc(), so requests over a direct connection (an API key, a job) call functions too.rpcTransportand the jobs block'spgmq_publicbackend accept a typedSupabaseClient<Database>without a cast.- Breaking:
gentypes function results as nullable: scalars,setofscalar elements, a single row and eachreturns tablecolumn are| null. Setfunctions.<name>.notNullinbetter-supabase.config.tstotrueor to the columns that are never null. - Breaking:
$rpcreturns rows of a table andreturns tablerecords in the configured casing with the configured codecs, like repository reads, typed as the model row. Delete hand-written key mapping around those results, or pass{ raw: true }for what PostgREST sent.
-
New SaaS blocks, each an SQL module with a subpath:
better-supabase/blocks/api-keys,/audit,/settings,/usage,/billing,/flags,/comments,/attachments,/data-lifecycle,/sso,/onboarding,/waitlist,/announcementsand/push. Add one withbetter-supabase sql add <name>;stripeis an optional peer that only billing and usage load.- SSO serves SCIM 2.0, flags evaluate rollouts in SQL and through an OpenFeature-shaped provider, and
SPEC_PINSgainsocsf,openfeatureandscim. Under theprovidermodel, SSO and waitlist roles go throughroleThroughand needcan_assign(SSO_ROLE_FORBIDDEN,WAITLIST_ROLE_FORBIDDEN), andjoin_waitlistalways answersstatus: "waiting"to clients. - Blocks take
ctx.postgresfrom@supabase/server,withBlock(key, create)puts a block on the context, and every block takestemporaland aproblemoption (ProblemFormat) for error bodies. - New event types cover organization domains and deletion,
billing.*, comments, attachments, data exports andwaitlist.approved. - Breaking: the server's
auth.kindgains"apiKey", so an exhaustiveswitchover it needs the new case.
- SSO serves SCIM 2.0, flags evaluate rollouts in SQL and through an OpenFeature-shaped provider, and
-
withBetterSupabase(server, options)frombetter-supabase/serveris one@supabase/middlewareentry that resolves the caller and contributes the request's database handles, and the server gains session and account helpers.better-supabasedepends on@supabase/server^1.9.1.- The entry reads a bearer token, then the session cookie, enforces
allow,aalandscopes, and contributesbs,db,sql,tenantand the keyswithSupabasewrites.server.context(request)runs the same entries once per request.createServer(betterSupabase, { db: { timeout, retry, urlLengthLimit } })tunes its requests. - Session cookies support the
@supabase/ssr0.12tokens-onlyencoding (encode: "tokens-only", doctor BS412 for mismatches), andcookieScopesandclearSessionAtScopesexpire cookies an earlier deploy set. sessionStatusandclearSessionCookiesrun the ended-session check in any proxy.endSessions(sql, userId)andsuspendAccount(admin, sql, userId, { suspended })end sessions and ban a user, anddeleteAccountis exported on its own and returns the database's error, such asORGANIZATION_OWNER_REQUIRED, with thesqloption.- An early refresh that hits a network failure keeps the user until the token expires, and the
otelplugin recordsdb.$rpccalls as spans. - Breaking: the leaf entry
withBetterSupabase(betterSupabase)that ran afterwithSupabaseis renamedwithBetterDb(betterSupabase). Replace it, or dropwithSupabaseand use the newwithBetterSupabase(server).
- The entry reads a bearer token, then the session cookie, enforces
-
The settings block has a
platformscope for product-wide settings, and feature flags can be managed from an admin page.defineSettings({ platform })addsclient.platform; each key names the permission that changes it and who reads it, and only listed keys can be written.createFlagAdmin({ transport })lists, saves and deletes flags and sets overrides for staff withflags.manage.tenant_ids_with_flag(key)is the policy form offlag_enabled,createFlagsProviderworks over the Data API, andflagContextreads amembershipsclaim.
-
SQL modules call each other through shared helpers and declare the modules they work with, and fewer modules pull in others they only use when present. Run
better-supabase sql sync.ModuleContextgainsrecord,notify,enqueue,entitlements,broadcast,canandstaff. Each returns a statement or expression that is valid without the other module, so a module installs alone and calls the other one once it is installed.SqlModule.integrateslists the optional modules a module works with.ctx.installedandctx.ofthrow for a module it neither requires nor lists,better-supabase sql listprints "works with", and the blocks overview has the full matrix.inboxno longer requiresjobsorstreams: withoutjobsit queues no bot or delivery jobs.workflowsno longer requiresjobs, andai-cacheno longer requirestenant.webhooks-inrequiresupdated-at, which its trigger uses.support.endedrecords the session's tenant in the audit log and the outbox, likesupport.started.sql.modules.jobs.schemais rejected: the jobs module always installs inbetter_supabase, so a schema only pointed the modules that call it at missing functions. Functions of theaccessmodule are always called inbetter_supabase, whateversql.modules.access.schemasets for its tables.
-
SQL modules can be called over the Data API without exposing their schema, and land their extensions and event triggers in migrations. Run
better-supabase sql syncand create a migration for the new function bodies.sql.modules.<module>.apiwritessecurity invokerentry points for the functions apps call into a schema, andrpcTransport(supabase, { schema: "api" })calls them there. Doctor BS312 points at the option.sql add,syncandupgradewrite the modules' extensions into a<stamp>_better_supabase_extensions.sqlmigration, and data files repeat extensions and event triggers so pg-delta plans need no hand edits (doctor BS321, BS323).- The
ensure-rlsmodule enables row level security on every new table outside the Supabase-managed schemas. - A write that breaks a
jsonb-schemasschema fails with avalidationerror whoseissuesname the column. sql.modules.sessions.options.policieswrites a restrictivebs_session_activepolicy on every table, and doctor BS320 reports tables without one.- The
grantsmodule keys functions by signature, takes column privileges such as"update(title, body)", and derives grants from permissive policies withoptions.fromPolicies. - Policies check tenant permissions once per statement, module functions pass
supabase db lint, and module actions follow a renamed sibling key. - Breaking:
grantswrites the complete privilege set of each entry inexpose, so a table loses privileges it doesn't list, includingtruncate,referencesandtrigger, on the next sync.
-
defineBucketcovers what Storage added in supabase-js 2.117, takes several path layouts and reports bad paths as aResult.versioningandlifecycle, applied withbucket.apply(client)and checked by doctor BS302. Connected buckets listversions, take aversionId, and haveremoveVersions,purgeCache,copyandmove.defineVectorBucketanddefineAnalyticsBucketdeclare the new bucket kinds.path: [current, older]accepts several templates, and{...rest}matches any depth.defineBuckets({ ... })registers definitions by id for rows that store a bucket id (byId,resolve,connectStored).- Policies from
.sql()use the name index,sweep()removes only objects matching everywithinvalue, andreplace()checkspreviousagainst the tenant. - Breaking: a bucket definition's
path(values)returns aResult<StoragePath>instead of throwing. - Breaking: a connected bucket's
publicUrl()returns aResult<string>.better-supabase codemod 0.6lists both kinds of call.
-
The tenant module keeps references inside one tenant and reads membership roles through a roles table.
sql.modules.tenant.options.sameTenantentries add a trigger that fails a write referencing another tenant's row withTENANT_MISMATCH, also throughmatchandthroughcolumns.options.roleThrough: { table, id, column, where?, tenant? }reads role names when memberships store role ids, taken from the provider'sroleSourcesunder theprovidermodel. Abs_role_scopetrigger refuses roles outsidewhereon direct writes (MEMBERSHIP_ROLE_SCOPE), and doctor BS324 reports a shared roles table withoutwhere.
-
better-supabase/testingchecks Next.js navigations for instant renders and database budgets, and its kits check adapters and executors.expectInstant(page, options)runs a navigation inside@next/playwright'sinstant()(an optional peer) and checks what shows while the lock holds.expectDbBudget(response, { maxCalls })checks thewithDbStats()header and waitssettleMsfor late responses.testAdapter,testExecutorandtestPlugincheck custom adapters, executors and plugins against the first-party contracts.
-
The usage block (
better-supabase/blocks/usage) meters fractional quantities against quotas that follow the plan or billing cycle, and entitlements read from any plan catalog.- Quantities and limits are
numeric,recordManyandconsumeManyrecord several meters all or none, and anulllimit is unlimited. options.metersis a meter catalog in the config or a table, read byusage.meters(),overview(organizationId)andcurrent().options.historykeeps each record, read byhistoryandbreakdown. Reading needsusage.readand recordingusage.record.- The
billingperiod followsusage_billing_period(tenant), andreportUsageToStripe({ overage: true })sends only usage above the quota. entitlements.sourcereads features from your plan tables or"custom", with values throughentitlement_value, andentitlements.claimsets the claim's shape (EntitlementClaimOptions).
- Quantities and limits are
-
Vector search supports hybrid ranking, filters and scores. Run
better-supabase sql sync.vectorSearchentries taketype: "halfvec",hybrid,boost,prefilter,predicateandorder.db.$searchtakesfilter,textandscore: true; on ahybridentrytextalone ranks by full text when the embedding call fails.options.schemasets pgvector's schema, whichsql syncotherwise reads from yourcreate extensionstatement.
-
Breaking:
createInboxinbetter-supabase/blocks/jobsis nowcreateWebhookInbox, and its types areWebhookInbox*; the old names are removed.better-supabase/blocks/inboxhas a differentcreateInboxfor the conversation inbox. Runbetter-supabase sql upgrade.- Messages record a tenant and dedupe per source and tenant, and
message.checkpoint(fields)saves progress a retry resumes from. store(event)stores an event an SDK already verified, and a source without secrets is store-only.- Sources take
maxAttempts,processtakesbudgetMsand renews leases, and bodies overmaxBodyBytes(1 MiB) get a 413.
- Messages record a tenant and dedupe per source and tenant, and
-
The
webhooks-inmodule andcreateIncomingWebhooksinbetter-supabase/blocks/webhooksgive each tenant trigger URLs with hashed tokens, andcreateSafeFetchcalls URLs that users supply.- Endpoints verify Standard Webhooks, HMAC-SHA256 or a shared secret, with size and rate limits, keep secrets in Vault, and hand deliveries to the webhook inbox.
update,rotateandrotateSecret(id, { grace })change them, andoptions.subjectsattaches them to a record. verifyWebhookand incoming endpoints accept Svix'ssvix-*headers.createSafeFetch(options)allows public HTTPS URLs only, checks every redirect, drops credentials on a redirect to another origin, and throwsUnsafeUrlErrorfor a refused URL andUrlCheckErrorfor a failed check such as DNS.
- Endpoints verify Standard Webhooks, HMAC-SHA256 or a shared secret, with size and rate limits, keep secrets in Vault, and hand deliveries to the webhook inbox.
-
The
workflowsandworkflow-builderSQL modules record durable workflow runs and store graph workflows per tenant.better-supabase/blocks/workflowsrecords the runs of any engine inworkflow_runswith cron schedules, semaphores and admission control, anduseWorkflowRunsanduseWorkflowRunin/blocks/workflows/reactread them.better-supabase/workflow-sdk/worldis a Workflow SDK World on Supabase (Node only,createWorld(), theworkflow-sdk-worldmodule), andbetter-supabase/workflow-sdkaddsstartFor,workflowStarter,startOnEvent,authorizeHook,hookMetadataandprotectWebHandler.better-supabase/blocks/workflow-builderstores definitions, checked versions, triggers, credentials bycredential_ref, a step library, node status and alerts, withuseWorkflowBuilderanduseWorkflowCanvasRun.better-supabase/workflow-sdk/builderaddscompileGraph,graphStarterandnodeRunReporter. The new permissions areworkflow.editandworkflow.publish.- Only the service role stores a version's compiled form (
createBuilderwrites it throughservice), andgraphStartercompiles from the graph on every start. Node ids are 1 to 100 letters, digits,_or-. - The World refuses unsigned poll deliveries outside development and test, fails closed on a Vault read error, aborts a delivery after
deliveryTimeout(300 s), and mirrors a run's tenant only when its actor holdsworkflow.runthere.
-
verify_api_keyrefuses a personal key while its user is banned in Supabase Auth (banned_untilin the future) or soft-deleted. Before, only the configured disabled column stopped a personal key, so a banned user's key kept working after their sessions ended. -
The
data-lifecyclemodule (version 5) exports and purges more module tables, leaves credentials and audit snapshots out of exports, and the organization purger revokes credential refs before the purge. Runbetter-supabase sql syncto get the new version.- Platform role assignments, outbox events, webhook secrets (purged, never exported), inbox tables, flag overrides and SCIM users per user, waitlist redemptions, AI provider keys, connector servers and grants, and workflow credentials join exports and purges.
- Exports leave out every
credential_refcolumn, and audit events loseold_record,new_recordand the impersonation columns. createOrganizationPurger({ credentials })revokes thecredential_refof each row the purge deletes through theCredentialProvider(connector grants for their user), only when the ref carries the organization. The rest come back inpurge.credentials.unrevokedwith a reason (foreign,no_providerornot_revocable), and a failed revoke stops the purge before it deletes anything.- The purger now checks that the deletion is due (
organization_credential_refs) before it cancels billing or clears Storage.
0.5.1 (2026-10-04)
-
Breaking (types):
SessionActoris a union onkind:oauth-client(withchain),support(withsessionId,readOnlyandreason) andimpersonation(withreason), andImpersonatorgainskind. An exhaustiveswitchonsession.actor.kindneeds the two new cases.Support sessions and impersonated sessions mark their
actclaim:supportClaimswritesact.kind: "support"andactingAswritesact.kind: "impersonation".actorOfreads them as their own actor kinds instead of OAuth clients,session.impersonatorfollowsactorOf, so an OAuth client or agent chain is no longer shown as an impersonator, andsession.delegationand thescopesguard apply tooauth-clientactors only. Anactwith anotherkind, or a support level withoutsession_id, makes the session invalid (reason: 'actor'). A support token minted by 0.5.0 (session_idwithoutkind) still counts as a support session until 0.6.supabaseClaimFixturesfrombetter-supabase/testingholds a support session (writable and read-only), an impersonated session, an OAuth client and an agent chain, each valid against PermDock'ssupabase-claims-v1.json. -
Breaking:
allow: ['anon']no longer admits users fromsignInAnonymously(). Only'anonymous'admits anonymous sign-ins, and'anon'means a caller without a session.['anonymous']alone now admits anonymous sign-ins and refuses other users; before, it admitted nobody. A route that serves guests next to signed-in users lists['user', 'anonymous'], and a public route that also serves guests['user', 'anonymous', 'anon']. With PermDock, pair the defaultallowwithrls: { anonymousSignIns: 'deny' }inpermdock.config.ts. -
The entitlements module in PermDock mode reads
entitlement_members()from the manifest'srls.membershipsentries for its scope, the tables PermDock'smember_<scope>_ids_forreads, and falls back to the hook's membership sources only for a manifest withoutrls.memberships. Doctor (BS408) warns whenrls.membershipsmaps no table to the scope, and notes the fallback. -
The
permdockaccess model reads PermDock's manifest itself.better-supabase sql addtakes the helpers' schema fromrls.schema, the scope from the rootrls.scopesentry (the one withoutwithin) and the id type from that scope'stype, whateverentitlements.permdocksays, so a manifest withrls.schema: "authz"and a root scopetenantgetsauthz.permitted_tenant_ids. It stops instead of guessing when the manifest is missing, has no single root scope, or disagrees withkits.access.permdock.scope,kits.access.permdock.schemaorkits.access.idType; 0.5.0 fell back topermdock.permitted_organization_idsanduuid. Code that renders the kit without the CLI (renderKit,moduleBody) now sets bothkits.access.permdock.schemaandkits.access.permdock.scope. Doctor reports the same problems, and helpers the manifest or the database lacks, as the new BS411 error. -
Under the
permdockaccess model,can_user()andmember_can()raise SQLSTATE0A000(hintACCESS_CALLER_ONLY) for anyone but the caller instead of returningnull, because PermDock's helpers readauth.uid(). The invitations module no longer re-checks the inviter's permission when an invitation is accepted, and the notifications module no longer filters recipients by their read permission, under that model; in 0.5.0 the re-check passed silently on thenullanswer, and the filter dropped every recipient but the sender. Check both in the app when they matter. Runbetter-supabase sql addto rewrite the installed files. -
The access kit docs, the
canAssignJSDoc and thecan_assign()SQL comment recommend PermDock's assignment rule for thepermdockmodel:canAssign: "permdock.permdock_can_assign({role}, {tenant}::text)", with the manifest'srls.schema. This is the assignment rule the 0.5.0 note "thepermdockandcustomaccess models need an assignment rule" refers to; see the access kit. Without it only the service role assigns roles, since PermDock projects don't install thetenantmodule, and doctor (BS411) warns. -
Under the
permdockaccess model,better-supabase sql addand doctor (BS411) check every permission key the kit modules check againstpermissions.catalog.json, platform keys forpermdock_hasincluded. A key must berowConditions: false, because PermDock's helpers check role and scope only; a key with row conditions, without the flag or missing from the catalog stopssql add.kitPermissionKeys(kits, names)frombetter-supabase/sqllists the keys with their module, action and scope, andkitFilePaths(names, layout)lists the files a set of modules writes without rendering them, sosql listworks while the access settings are incomplete. -
better-supabase genuses@supabase/postgrest-typegen0.4.0.database.types.tsgains aComputedFieldskey on every table and view: the names of its computed fields, ornever. postgrest-js reads it to leave computed fields out ofselect('*'), and row-typed function arguments no longer include them. Runbetter-supabase gento regenerate. Supabase CLI 2.119 doesn't writeComputedFieldsyet, so until it does,supabase gen typesoutput differs fromgenby that key. -
The optional
oxfmtpeer accepts any version from 0.66.0 below 1.0, sopnpm add -D oxfmtinstalls the latest. The runtime dependencies are ranges instead of exact versions:@supabase/postgrest-js ^2.116.0(the floor of the@supabase/supabase-jspeer),@supabase/ssr ^0.12.7,@supabase/server ^1.9.0,@supabase/middleware ^1.0.0and@standard-schema/spec ^1.1.0, so an app shares one copy of each with its own Supabase packages.@supabase/postgrest-typegenstays an exact version.The
canAssignJSDoc no longer says it defaults to true. Thecustomaccess model requires it. Without it, thepermdockmodel lets only owners assign the owner role when thetenantmodule is installed, and lets only the service role assign roles when it isn't. The access kit docs now say the same, and theircustomexample setscanAssign, which that model requires.
0.5.0 (2026-10-04)
-
Guards refuse anonymous users (
signInAnonymously(), theis_anonymousclaim) with a 403ANONYMOUS_USERunlessallowlists'anonymous'or'anon',toSession()addsanonymous, and therealtime-tableskit module sends anonymous users no change signals. Before 1.0 this minor release is the breaking slot: routes that served guest sessions needallow: ['user', 'anonymous'].The framework adapters agree with each other.
ctx.apply(response)adds refreshed session cookies and, after a write,bs-primary-until, so Hono, edge and the new oRPCbs.fetchHandler(handler)keep read-your-writes like Next.js does. MCP reads the session from theAuthorizationheader only, answers an unreachable JWKS with a 503 instead of a scope challenge, sends a missing second factor as a plain 403, takesadvertisedScopes(withscopesas a deprecated alias) and enforcesrequiredScopes. REST resources refuse cross-site form posts that ride on the session cookie (CROSS_SITE_REQUEST) and answer malformed keys and bodies withinvalid_input, whose detail survives in production. Hono'sbs.onErrorreturns anHTTPException's own response, Next.js route handlers turn unexpected throws into a 500 Problem Details response whileredirect()andnotFound()still work, andcontextForSessionrequires the token's auth kind to match the session.createServertakesfetchfor its PostgREST and supabase-js clients, theauthevent carriesreasonandrawSource, thereact-serverbuild exportshasEntitlementanduseLiveCount, and every adapter builds the user actor, impersonator included, from one function. -
The
auditSQL kit module can adopt an existing audit table (kits.audit.tablesandcolumns), andaudit()takesredact,category,event_prefix,target_typeandtenant_column. The newaudit_eventfunction records events that are not row changes, with an idempotency key. The optionsappendOnly,readPolicy,impersonators,restricted,eventRoles,eventCategoryandeventSourceadd an append-only guard, a tenant read policy, hidden impersonation columns and a separate table for sensitive details.purge_audit_loghonours anaudit_retention(tenant)SQL hook, andpurgeAuditLogfrombetter-supabase/jobstakes a per-tenant retention callback. Runbetter-supabase sql upgradeto move from version 1. -
better-supabase/honoaddsbs.app(), aHonoapp typed with the adapter'sEnvand withbs.onErrorinstalled, and the type-onlybs.Envfor apps that build their own. After@supabase/server'swithSupabase,bs.middleware()reuses its verification of the bearer token when the stored claims are that token's payload, then runs its own claims,actanduserMetadatachecks.clearOnUserChange(queryClient, auth)inbetter-supabase/queryremoves the["bs"]queries when the signed-in user changes, for apps without React;BetterSupabaseProvideruses it.A bucket with
tenantnow checks paths on the client too:upload,download, signing,remove,reserveandlistrefuse a path in another tenant's segment, or any path when the connection has no tenant, with aforbiddenerror before calling Storage. Pass{ context }or{ tenant }toconnect(), or{ allTenants: true }for cross-tenant admin work;deleteAccountdoes the latter.client.path(target)returns the checked path.Jobs record the enqueuing request's actor and tenant:
enqueue(queue, payload, { context })andschedule(..., { context })store them next to the payload, and the handler gets them asjob.context, ready fordb.$with(job.context). A job without a tenant gets thetenant()plugin'sonMissingunless the worker runs withallTenants: true.actor()fills animpersonatedBycolumn (generated fromimpersonated_by, the column the SQL kit'strack_actorstamps) from the impersonating admin, and clears it on writes without one.Event sink sends are tracked on
betterSupabase.events(pending,settled()). Next.js routes and actions hand them toafter(), and edge handlers towaitUntil: the Workersctx, orcreateEdge(..., { waitUntil })on Supabase.The PermDock guide has oRPC and Hono recipes that refuse a procedure or route without a permission.
-
Jobs run without pgmq or pg_cron when you want.
kits.jobs.options.backend: "table"stores jobs inbetter_supabase.job_messagesand claims them withfor update skip locked, andkits.jobs.options.scheduler: "drain"stores schedules with a time zone each.jobs.drainRoute({ secret, handlers })is a route for Vercel Cron that enqueues due schedules and drains queues within a time budget, anddraintakes abudgetMs.scheduletakes{ timeZone }and validates the cron expression first.createJobsaccepts anyQueueBackend(API version 1), withsqlQueueBackend,pgmqPublicBackendand thetestQueueBackendconformance kit. Thejobsmodule is now version 2:better-supabase sql upgradedrops the old four-argumentschedule_job. -
The SQL kit's rows and role settings, which a schema diff can't capture, move to
supabase/better-supabase-data, and the newbetter-supabase sql datawrites them into a migration stamped after the newest one.rate-limitsetspgrst.db_pre_requestthat way, and doctor BS313 warns when the live database never callscheck_request().sql upgradewrites next to the locatedconfig.toml, and each module rejects options it doesn't declare.Breaking:
entitlementsno longer defaultsentitlements.customertoorganizations.stripe_customer_id. It reads the managedorganizationsmodule's column when that module is installed; otherwisesql addstops until you set it.tenant_ids_with_entitlement(key)gives policies a set check.Jobs enforce max attempts at claim, retry with full jitter and replay dead letters (
replay_dead_job,jobs.replay). The outbox orders by(xid, position)and gives events uuid ids. Outgoing webhooks take a lease token, follow the Svix retry schedule withRetry-After, and disable a destination after failing fordisableAfter. Notifications back off between deliveries and fail them aftermax_attempts. Key indexes lead with the key, webhook policies readtenant_ids_with()once per statement,jsonb-schemasadds its checksnot validand validates them separately, andpurge_rate_limits(),purge_webhook_deliveries()andpurge_notifications()join the other purges. -
Kits share one set of extension points: typed events, policy callbacks, SQL hooks and fixed attribute names.
betterSupabase.on("kit", handler)receivessupport.*,org.*,invitation.*,notification.*andwebhook.*events with a copy of their data.onKitEvent(betterSupabase, "support.*", handler)frombetter-supabase/eventsfilters by type or prefix and types the data.forwardKitEvents()andkitCloudEvent()send kit events to anEventSinkas CloudEvents, andtraceKitEvents()frombetter-supabase/otelrecords them with theKIT_ATTRIBUTESnames (better_supabase.org.id,better_supabase.support.session_id, ...).- Policy callbacks (
Policy,PolicyDecision) fail closed: onlytrueallows, and a throw or rejection denies. kits.<module>.hookssets where a module looks for the app'sbefore_*andafter_*SQL functions, andkits.<module>.events: falsestops it writing its events to the outbox.
-
Breaking: the SQL kit fails closed.
current_tenant_id()returns a tenant only while the caller is a member, the claim is cleared when a member is removed, andkits.access.activeTenantdefaults to'resolver'(the tenantServerOptions.tenantresolved, then the claim). Disabled organizations deny access, thepermdockandcustomaccess models need an assignment rule, and only an owner can transfer ownership. A member can never raise their own role or assign one above it.Platform invitations move to
platform_invitationsunder a role ceiling that accept checks again, accept checks that the inviter can still assign the role, andvalid_foris capped bymaxValidFor. Support sessions refuse platform targets and writes by default and allow one active session per admin; a token with anactclaim has no platform permissions.Doctor BS312 reports a kit schema listed in
[api] schemas.set_actorkeepscreated_byon updates,track_realtimerefuses a table without the tenant column, the rate limit andrequest_ip()use the right-most forwarded hop, audit is append-only by default, idempotency keys are scoped to the caller, public buckets get no select policy, members see only public profile columns, and outgoing webhooks follow no redirects, cap the response body and use 32-byte secrets. -
Breaking: the SQL kit follows one naming standard. Run
better-supabase sql upgrade, thensql syncandsql data; the 0.4 to 0.5 guide lists every step. The forward steps renamememberships.org_idandinvitations.org_idtoorganization_id,better_supabase.audit_logtoaudit_events(withoccurred_atandorganization_id, and a read-onlyaudit_logview until 0.6), andaudit_trigger()toaudit_row_change(). Doctor reports the old column names in your SQL (BS309), also when a column is unqualified next to its table.The modules new in this release use the same names:
webhook_endpointswithevent_typesand the statusespending,delivering,succeeded,retrying,deadandcanceled; notifications withtype,data,actor_idanduser_id, andtypesinstead ofkindsincreateNotifications. Permission keys follow<area>.<verb>, and managed tables getupdated_attriggers and indexes on their foreign keys.Breaking:
toCloudEventsandkitCloudEventput the actor indata.actorIdinstead of theactoridcontext attribute, so user ids stay out of broker headers. The outbox relay prefixes event types withdev.better-supabase, andwebhooks.sink()removes the prefix again. -
The new
sessionsSQL module addsbetter_supabase.session_active()for restrictive policies: it is false once the token's session was revoked or expired, or the user is banned or deleted.Options that only exist to match an existing schema are accepted in
mode: "adopt"only:tokenStorage: "plain"for invitations,secretStorage: "column"and non-texteventIdTypeorrunIdTypefor webhooks-out, andkitSourceordefaultSourcefor the outbox. Doctor BS314 warns about each until you remove it.better-supabase/sqlexportsmigrationOptionUses(kits)to list them. TheassignmentCeilingandtriggerPrefixoptions, the invitationserrorCodesoption andkits.access.disabled.tenantKeyare removed. -
SQL kit modules now fit existing schemas.
kits.<module>in the config sets a module's mode (managed,adoptover your own tables, orcustomwhere you write the contract functions), its schema, table and column names, tenant id type and permission keys. Each file's header records the module version and mode, and schema files record them inbetter_supabase.kit_modules. Doctor BS307 checks the functions of custom-mode modules, and BS304 points hand edits atkitsinstead.The new
accessmodule gives policies and kit modules one permission check,can(scope, id, permission), withtenant_ids_with(),is_platform(),can_user(),can_assign()andpermission_claims().kits.access.modelpicks a role list from the config, role and permission tables with per-tenant overrides, PermDock or your own functions, andkits.access.disabledswitches off tenants and users with adisabled_atcolumn. Thetenantmodule (version 2) takes its role names fromkits.access.roles, addsmemberships.last_used_atandorg_member_role(), writes the memberships claim as an array or a map (options.claimFormat), and reads the active tenant fromkits.access.activeTenant: a resolver (the default), the claim or a profile column.For resolvers,
createServertakestenant: (request, auth) => id, andcontext()takes{ tenant }. The tenant becomescontext.tenant, thebetter_supabase.tenantsetting over Postgres and thex-bs-tenantheader (TENANT_HEADER) over the Data API.postgres.asUser,executorForandtransactiontakesettingsandreadOnly. Doctor BS308, with--as <user id>, warns when the access token hook writes no tenant claim while the claim is the active tenant.better-supabase/configexports the kit config types (KitsConfig,KitModuleConfig,KitMode,AccessKitConfigandActiveTenantSource) for apps that build theirkitsconfig in a separate module. -
SQL kit modules have versions and an upgrade path, and a codemod rewrites renamed APIs.
better-supabase sql upgradereads each module's@bs-kitversion (a file without one is version 1), writes the forward steps intosupabase/migrations/<timestamp>_better_supabase_kit_upgrade.sqland rewrites the module files.--checkexits 1 when a module is behind or a file is stale.- Deprecated kit functions keep a wrapper under the old name until they are removed.
upgradePlan(),kitDeprecations()andmoduleVersion()are exported frombetter-supabase/sql. track_updated_at()andaudit()warn about an existing trigger that does the same work, and drop it withreplace_trigger => true.- Doctor reports deprecated or removed kit symbols in schema files and policies (BS309), a kit trigger next to an equivalent one (BS310), and a module behind its version on disk or in
better_supabase.kit_modules(BS311). BS304 skips files BS311 reports. better-supabase codemod <version>rewrites imports, members, JSX props and call options for renamed APIs (0.4, and0.5forcreateMcp'sscopes), skipping strings and comments, and lists the lines it leaves for review.--dry-runprints the diff.
-
Add the
notificationsSQL kit module andbetter-supabase/notifications.notify(jsonb)is asecurity definersender that checksnotifications.send, leaves out the actor and non-members, and applies subject subscriptions and per-channel preferences.createNotificationssends with typed data and reads, counts, marks, dismisses and resolves notifications;deliver()sends email, push and other channels throughNotificationChannelimplementations with leases and retries.useNotificationsfrombetter-supabase/reactkeeps a list current over a private Realtime topic. Existing tables work withmode: 'adopt', and the topic and event names are configurable.OrgsTransportis nowKitTransport, exported from bothbetter-supabase/orgsandbetter-supabase/notifications. -
Organizations and invitations. The new
organizationsSQL kit module creates, updates and deletes organizations (hard or soft), changes member roles, removes members, transfers ownership and switches the active organization throughkits.access.activeTenant. A deferred trigger keeps an owner in every organization, and a trigger stops members from raising their own role or assigning one above it. Theinvitationsmodule moves to version 2:invite_memberreturns the invitation with its token, invitations can be resent, revoked, declined and previewed without a session, can carryprefilldata and can target the platform under the catalog access model, and accepting re-checks that the inviter may still invite.create_invitationkeeps its 0.4 signature. Both modules check permissions through the access contract, soinvitationsnow needsaccess, and both supportmode: 'adopt'with options for token storage, slug rules, error SQLSTATEs and extra columns. The newbetter-supabase/orgssubpath calls them withcreateOrgs, over Postgres (sqlTransport) or the Data API (rpcTransport), withcanInviteandonInvitecallbacks andorg.*andinvitation.*kit events, including the neworg.updatedandorg.deleted. Thetenantmodule now installs on a fresh database underkits.access.model: 'catalog'. -
Transactional outbox. The new
outboxSQL kit module stores events withemit_event(type, payload, subject, tenant, key, source)in the writing transaction, deduplicates by key per tenant, addstrack_events(table)row triggers and keeps events foroutbox_historyuntilpurge_outbox. Named consumers read in order with their own cursor and a lease, and a claim never skips an event whose transaction is still open. Theorganizationsandsupport-sessionsmodules write their events to it once it's installed.createOutboxinbetter-supabase/jobsemits, registers consumers, relays events as CloudEvents to anyEventSinkand servesrelayRoutefor a cron caller. The module supportsmode: 'adopt'for an existing events table.The
defaultSourceoption fills the source ofemit_eventcalls that pass none, andkitSourcesets the source kit modules write (better-supabase/{module}by default), so an adopted events table with a check on its source column keeps working. Adopt mode still creates the consumers table, since an existing app has no cursors yet. -
The PermDock integration fails closed in more places. Buckets and topics in
permdockmode now call PermDock's helpers inpermdock, PermDock's defaultrls.schema, instead ofpublic; setschema: 'public'on the policy if your PermDock config writes them there.genand doctor BS214 refuse PermDock bucket policies whenpermissions.catalog.jsonis missing, and the catalog must be version 1.sql add entitlementsrefuses a PermDock project without a manifest orrlsblock instead of falling back to the tenant module (setentitlements.permdock: falsefor that). BS214 also reports a helper schema other than the manifest's, a scope the manifest doesn't declare and a key checked at another scope than its catalog entry. BS408 checks that PermDock's hook fillsclaims.featuresfrombetter_supabase.feature_claimsand that a membership source covers the scope. The new BS409 reports aclaims.tenantthat differs from the manifest'srls.tenantClaim, PermDock markers other than v1 and aclaims.scopethat isn't the root scope. Scope id typesinteger,int8and other aliases are accepted, and MCP table tools take a permission per operation from the resource'smeta. -
Plugins compose without holes.
timestamps(),actor()andsoftDelete()refuse caller-supplied values for the columns they fill unless the call passes{ override: true }, and generated JSON Schema and OpenAPI documents mark those columnsreadOnly. On tenant tables, an upsert that updates on conflict needs the tenant column in its conflict target, the SQL executor guards the update withwhere <tenant> = excluded.<tenant>, a query whose includes reach a tenant table fails closed without a tenant, and numeric tenant columns compare by their text. An upsert that updates a soft-deleted row restores it.rules()runs before every other plugin.noUnboundedFindManyskips aggregates,maxLimitignorespaginate()'s look-ahead row,requireTenantContextreads the same claim paths astenant(), andnoSensitiveSelectchecks reads only, because writes without aselectno longer return sensitive columns. Offsetpaginate()orders by the primary key by default.validation()decodes codec columns (Temporal values,bigint) before validating and encodes them afterwards.Mutation events give hooks and listeners a copy of the rows, so they can't change the result (
testPluginchecks this), and carryintent(softDeletefor a delete that became an update), the affected primarykeyswhen they are known, and thetenantthattenant()resolved. CloudEvents and cache invalidation use them, so soft deletes sendrow.softdeletedevents and invalidate their rows.defineReadSetwarns when query plugins scope tables its generated function reads. The audit kit keys entries by each table's primary key and reads the tenant column fromplugins.tenant.column, andgenrefuses a soft-delete column that isn't a timestamp. -
Profiles and image buckets. The new
profilesSQL kit module creates a profile on sign-up from auth metadata (full, first and last name, avatar), allocates a unique username, mirrorsauth.users.email, grants updates only on the columns users own and refuses changes to service-owned columns (PROFILE_COLUMN_READONLY). It supportsmode: 'adopt'for an existing table keyed by any column, anafter_profile_synchook, extra columns, a read policy for people in the same organizations andbackfill_profiles().better-supabase/storageaddsavatarBucket()andorgLogoBucket()presets and a bucket policy,{ access: { read, write } }, that checks permissions through the SQL kit's access contract. -
Support mode: platform admins can view the app as a user. The new
support-sessionsSQL kit module records sessions in a table (or an adopted one), checksis_platform('support.start'), writessupport.startedandsupport.endedto the audit log and can call your access token hook for the target's claims.createServertakessupport: supportSessions({ store, authorize, claims, policy, cookie }), which apps without support mode never bundle; while thebs-supportcookie names a running session, contexts, sessions, actions and routes run as the target over Postgres, read-only by default, with anactclaim that carries the session id. Next.js getsbs.startSupport(),bs.stopSupport()andsupportTag(), React getsuseSupportSession(), andbetter-supabase/testinggetstestSupportSessionStore. The audit module records the session in a newsupport_session_idcolumn; an adopted table mapssupportSessiontonullwhen it has none. -
Values from Postgres and the peer libraries no longer break responses. JSON responses from the server adapters and MCP write
bigintvalues as decimal strings. Atimestamptzortimestampholdinginfinitycomes back as aninvalid_valueerror (status 500, withcolumn) instead of throwing, because Temporal has no infinite value. Temporal values from another realm or a second polyfill copy are recognized by theirSymbol.toStringTag, and Standard Schema issues with symbol path keys keep the error serializable.loadEnv()reads inline keys fromSUPABASE_JWKS, as@supabase/serverdoes, and verifies with them without fetchingjwksUrl. The OpenTelemetry plugin writesdb.namespaceas{database}|{schema}(postgres|publicby default,otel({ database })to change it), addsserver.addressandserver.portfromotel({ server })to spans and metrics, and setsdb.response.status_codeonly for SQLSTATE codes.BetterQueryMetabuilds on the app'sRegister['queryMeta'], andsend_email'semail_dataaccepts fields Auth adds later.Doctor's BS410 reports HTTP auth hooks and checks their
v1,whsec_secrets, and its custom access token hook event carriesissandamr. The peer ranges now state what the code needs:pg >=8.15 <9,@tanstack/query-core ^5.62.0,@orpc/server >=2.0.0-beta.40 <3,hono <5,next <17,@opentelemetry/api <2, andoxfmt 0.66.0, the version@supabase/postgrest-typegenpins; thegennotice and docs show how to allow a newer oxfmt. -
Add the
webhooks-outSQL kit module and outgoing webhooks inbetter-supabase/webhooks. Destinations subscribe to event types (exact,*orprefix.*), secrets live in Supabase Vault (or a column) and rotate with an overlap, and every delivery is logged with its status, response and duration.createWebhookspublishes events idempotently, dispatches to one destination, anddeliver()ordeliverRoute()sends due deliveries with leases, retries with backoff, dead letters, redelivery and auto-disable after repeated dead letters. Requests are signed with Standard Webhooks by default or withhmacSignerfor an existing format, andpublicUrl()rejects private and local addresses, also on each redirect.WebhookSigner,WebhookTransportandWebhookSecretStoreare versioned extension interfaces with conformance kits inbetter-supabase/testing. RLS policies in thenotificationsmodule now usebetter_supabase.can, which clients may execute, instead ofmember_can. -
The docs and skills follow the Naming page's file layout everywhere. The PermDock page defines
betterSupabaseinsrc/lib/supabase/index.tsand createsbsinserver.tsandclient.ts, and every Next.jsserver.tsstarts withimport "server-only". The docs drift check now fails a code block that breaks the layout. -
Doctor BS404 and
doctor --fix-grantsfollow pg-delta for PermDock's hook. With[experimental.pgdelta] enabled = true, they point topermdock supabase hook generate --out <schema file>without--grants-out, thensupabase db schema declarative sync, instead of a grants migration. A hook whose grants are in the declarative schema file that defines it now counts as granted under pg-delta, and the finding for a file that already grants the hook namessupabase db schema declarative syncbeforesupabase migration up. -
Doctor BS408 checks exactly the PermDock helpers the
entitlementsmodule calls for the chosen scope, and no longer claims PermDock writesmember_<scope>_ids_forfor every scope. It warns when the manifest lacksmember_<scope>_idsor doesn't letauthenticatedexecute it, when it lacksmember_<scope>_ids_for(add a membership source for the scope inpermdock.config.ts), and whensupabase_auth_adminmay not executemember_<scope>_ids_for(addsupabase.hook.claims: { features: 'better_supabase.feature_claims' }). -
The PermDock MCP recipe in the docs and the auth skill fails closed: a tool without a PermDock permission in
metais hidden (visiblereturnsfalse) and refused (authorizereturns{ allowed: false }). The page also shows PermDock'sdecidefor putting the denial reason in the refusal. -
The
entitlementskit module takes its PermDock scope from the manifest. Withoutentitlements.permdock.scope, it now uses the manifest's root scope (therls.scopesentry withoutwithin) instead oforganization, so a PermDock project whose root scope istenantworks without config. A manifest with no root scope or several stopssql add entitlementsand doctor BS408 asks forentitlements.permdock: { scope }. An explicitscopestill overrides,falsestill opts out, and a scope the manifest doesn't list is still reported by BS408.permdockKeyStatusand thePermdockCatalogandPermdockKeyStatustypes are exported frombetter-supabase/sql. -
In PermDock mode, the
entitlementskit module renders the tenant argument ofhas_entitlement,tenant_entitlementsandtenant_stripe_customer, and the rows ofstripe_customer_tenants, with the scope's id type from the manifest'srls.scopes(uuid,textorbigint) instead of alwaysuuid. A missing or other type stopssql add entitlementsand doctor BS408 reports it.better-supabase/sqlexportsKIT_ID_TYPES,isKitIdTypeand theKitIdTypetype, andKitPermdockhas a requiredidType. -
The auth skill's PermDock caching recipe works as written. It passes only static tags to
bs.cached(), then callscacheTag(snapshotTag(session.user.id))andcacheLife(cacheLifeFor(snapshot))inside the loader once the snapshot is built, because the user id and the snapshot don't exist beforebs.cached()returns. It pairs the recipe withbs.invalidateSession(userId, { tags: [snapshotTag(userId)] }). -
PermDock's catalog is read fail closed.
defineBucketanddefineTopicwithcatalogaccept only keys markedrowConditions: false: a key whose entry has no booleanrowConditions, or that the catalog doesn't list, now throws with a message to regenerate the catalog with a currentpermdock catalog. Doctor BS214 reports those keys as errors, andbetter-supabase genrefuses to write their bucket policies.
0.4.0 (2026-10-03)
-
The CLI ships inside
better-supabaseagain. Installingbetter-supabasegives you thebetter-supabasecommand, so drop@better-supabase/clifrom your dev dependencies (it was never published) and runnpx better-supabase initin a new project. The commands and their options are unchanged. The CLI's own dependencies are bundled into the package, so apps install nothing extra;pgstays an optional peer that the CLI needs to read your database.Import
run,registerCommand,defineCliCommandand the introspection helpers frombetter-supabase/cli.@supabase/configis an optional peer again, for readingsupabase/config.toml.VERSION,better-supabase --version, doctor reports and the header of newly written SQL kit files now show the package version instead of0.0.0. Existing kit files are not reported as changed, because the comparison ignores that version. -
Names are now the same in every adapter. The definition from
defineSupabaseisbetterSupabase, and every runtime instance an adapter creates isbs. They live inlib/supabase/index.ts,lib/supabase/server.ts(withimport "server-only"in Next.js) andlib/supabase/client.ts, which is whatbetter-supabase initnow writes. The old names have no aliases; the compiler points out each one. Naming lists the conventions.Before After createBrowser,BetterBrowser,BrowserOptions,BrowserAuthcreateClient,BetterClient,ClientOptions,ClientAuth<BetterSupabaseProvider browser={browser}>,BrowserLike<BetterSupabaseProvider client={bs}>,ClientLikeclient.sband thesboption oftestExecutorand the test pluginbetterSupabasenext.server()bs.context()(pass aRequestto read it instead of the incoming one)next.serverFor(session, { token })bs.contextForSession(session, { token })BetterEnvandbs.handle()(Hono)HonoEnvandbs.handler()bs.toORPCError()bs.toOrpcError()HandlerOptions(Edge)MiddlewareOptions, shared by Hono, oRPC and Edge and exported frombetter-supabase/serverhandler(MCP)endpointQueries,CreateQueriesOptions,queries.keyBetterQueries,QueriesOptions,queries.$keyPostgresBetterPostgresDefineSupabaseOptionsSupabaseOptionsrepository.$tablerepository.$tableNamecreateHono,createOrpc,createEdgeandcreateMcpnow keep the claims and profile types from.claims()and.userMetadata(), soc.var.auth,context.auth, the edge handler'sauthand MCP'sctx.authare typed by your schema.better-supabase/nextalso exports theLiveCountSeedtype thatbs.liveCount()returns. -
Less work per request in auth and the adapters.
- The Next.js adapter verifies the token once per render scope:
context(),session()andcached()share one resolution.BetterServergainscontextFromResolution(resolution, request)for the same pattern elsewhere. - A token seen before skips the claims and
userMetadataschemas as well as the signature check, per schema pair. createServerfetches the JWKS when it is created, so the first request doesn't wait for it. PassprefetchJwks: falseto turn it off; it is off underNODE_ENV=test.- Bearer requests parse cookies only when something reads them, and contexts read the replica pin cookie only when a read URL is configured.
- Job workers back off from
pollIntervaltomaxPollInterval(30 seconds by default) while the queue is empty, and keep the leases of every job in a claimed batch alive, not only the running one. - Webhook verification imports each secret's key once, compares signatures
with
crypto.subtle.verify, and checks the headers before reading the body. - The MCP server builds its tool list once when no
visiblehook is set. - The OpenTelemetry plugin builds span and metric attributes once per table and operation.
- Edge resource routes answer unknown paths before resolving auth, and a malformed id is a 400 instead of a 500. CORS header values are built once.
- The Next.js adapter verifies the token once per render scope:
-
A faster CLI that reads the database less often.
--helpand each command's--helpstart in about 30 to 50 ms instead of 130 to 150 ms: the commands, config loading, env validation, prompts and the typegen schemas load only when a command runs.genhashes the system catalogs with one query and reuses the snapshot cached innode_modules/.cache/better-supabasewhile the schema is unchanged.gen --watchkeeps one connection open and polls that hash, so an unchanged schema costs one small query per interval.- Introspection runs its queries concurrently over a pool of four connections. Connecting gives up after 10 seconds and each introspection query after 2 minutes; Ctrl-C ends the connection or Management API request and exits with code 130.
doctorreads the snapshot and runs its live checks and splinter over one connection, and reads its input files concurrently.oxfmtis now an optional peer. Without it,genwritesdatabase.types.tsunformatted and prints a notice instead of failing.- Generated files and doctor reports sort names by code point, so they no
longer depend on the machine's locale. Run
better-supabase genonce; the order of a few entries ingenerated.tsand the validator files can change. - Breaking:
parseSnapshotfrombetter-supabase/clinow returns a promise, so the typegen schemas load on first use. Addawait.loadSnapshottakessignalandcachein its source options.
-
Fixes four bugs the performance audit found, one of which changes behavior.
findManywithoutorderBynow orders by the primary key's database names. On a camel-cased table whose key isn'tid(customerTagswithcustomerIdandtagId) it sentorder=customerId.asc, which PostgREST rejects.topic.send()no longer closes a subscription on the same topic. realtime-js reuses the open channel for a topic, andsend()removed it after sending.liveQuerykeeps the shared channel when a listener re-joins in the same tick (React StrictMode remounts), instead of subscribing the new listener to a closing channel.- A session refresh now times out after
refreshTimeoutMs(5000 by default, an option ofresolveAuthand every adapter'sauth) and counts as a network failure. Before, a hung Auth request blocked every later request carrying the same refresh token. A rejected refresh is reused for ten seconds, like a successful one.
Breaking: where refreshing is off (Server Components, route handlers, prefetches, MCP), a token in its last 60 seconds is now valid until its
exp. It used to resolve as{ kind: 'anon', reason: 'expired' }, so pages rendered signed out in the last minute of every token.leewaynow only decides when the proxy refreshes. -
Generated files and types that cost less to check and to bundle.
- Breaking:
genwrites the schema metadata togenerated.meta.jswith agenerated.meta.d.tsnext to the main module, which imports it. TypeScript reads the metadata asSchemaMetainstead of checking a large object literal, which cuts check time and editor memory on large schemas. Runbetter-supabase genand commit both new files. - Breaking:
BetterPostgresgainsexecutorFor(claims), andcreateServeruses it forctx.sqlandactingAs(). Apps that don't passpostgresno longer bundle the SQL compiler; the Next.js, Hono, oRPC, edge and MCP entries are about 5 KB gzip smaller. A customBetterPostgresimplementsexecutorForaspostgresExecutor(asUser(claims)). - Reads without
selectorincludereturn the row type directly, which removes about a quarter of the type instantiations a query costs. - The build marks library modules as side-effect free and annotates module-level objects as pure, so bundlers drop the parts an app doesn't use.
- Breaking:
-
Faster request path in the core runtime.
connect()costs the same for any number of tables: every connection shares one prototype of table getters, and repositories are built on first access.- For a signed-in user,
ctx.db(anddbFor()) on the server runs on a bare PostgREST client, so requests that only query data no longer build the Realtime, Storage and Auth clients.ctx.supabaseandctx.db.$clientstill return the full supabase-js client, built when first read.better-supabasenow depends on@supabase/postgrest-jsat the version supabase-js uses. - Default selections, rendered select strings, column lookups, unique keys,
invalidation targets and the tables a query spec reads are computed once per
table or spec instead of on every call. Plugin hooks, tenant resolution and
claim paths are resolved once, and
mutationanderrorpayloads are only built when someone listens. - Row decoding and paging copy each row once instead of spreading and deleting keys in loops.
- Bucket connections take
{ cacheSignedUrls: true }to reuse a signed URL until shortly before it expires. The cache belongs to the connection. - Outside production, the event hub warns once when an event collects more than 50 handlers, which usually means a missing unsubscribe.
invalidationTargets()now returns a frozenreadonly string[]. -
The Agent Skills cover 0.3. A new
better-supabase-authskill teaches sessions, typed claims, OAuth clients and agents behind a token (session.actor,session.delegationand thescopesguard option),checkSessionbefore irreversible actions, what happens when claims change, and how to run next to PermDock. Thebetter-supabaseskill adds an upgrade workflow, cursor pagination,Temporalvalues, the CLI's--json,--db-url-stdinand exit codes, andmember_org_ids()in tenant policies. Thebetter-supabase-apiskill addsscopes, cursor resources, the MCPauthorize,visibleandallowedHostsoptions, the kit's purge functions and the Postgres pool options. Thebetter-supabase-testingskill adds delegated-token API tests,Temporalin tests and doctor in CI. Runbetter-supabase skills installto update installed skills.
0.3.0 (2026-10-02)
-
Name the OAuth client or agent behind a bearer token. A user session now carries
actoranddelegation, read fromclient_id,scopeand the RFC 8693actchain the way PermDock'sactorOfanddelegationOfread them. A malformedactchain resolves to{ kind: 'invalid', reason: 'actor' }and a 401, soInvalidReasongains'actor': an exhaustiveswitchover it needs the new case. Thescopesguard option onnext.route,next.actionand the edge, Hono and oRPC adapters answers 403 with an RFC 6750insufficient_scopechallenge when a delegated token lacks a scope; the user's own token is not limited.forbiddenerrors and Problem Details carry the neededscopes.better-supabase/servernow exportstoSession,AuthSession,ActClaim,SessionActorandSessionDelegation. A new monorepo guide shows one runtime package that ownsdefineSupabase, with domain packages typed from itsdb. -
Breaking: the
BetterResultShapetype is nowBetterResultValue. It is still the typetoBetterResultreturns, with thestatus,valueanderrorfields of a better-result value; rename the import to upgrade. -
On the Postgres executor,
createManyandupsertManysplit an insert that needs more than 65,535 bind parameters into several statements and run them in one transaction, instead of failing.upsertManysends rows sorted by the conflict columns, so concurrent upserts over overlapping rows lock them in the same order instead of deadlocking; its returned rows follow that order. -
checkSession(sql, auth)frombetter-supabase/authandbetter-supabase/serverconfirms that a user token's session still exists inauth.sessions, and returns anunauthorizederror with codeSESSION_REVOKEDwhen the user signed out or the session was ended. Call it before actions you can't undo, such as deleting the account. Nothing calls it by default, so other requests still verify tokens without a round trip. -
The CLI runs on citty.
initandaddask for the casing, the integrations and whether to overwrite files when they run in a terminal;--yes, CI and piped input skip the questions. Output is colored in a terminal (NO_COLORturns it off), database work shows a spinner on stderr, andgen --check,introspect --checkandsql sync --checkprint a unified diff of each stale file.registerCommandtakes a command from the newdefineCliCommand, andlist()splits list options. The 0.2 form,registerCommand(name, command, help), still works and is deprecated.help()replaces theHELPconstant. -
Breaking: the CLI follows one set of conventions for output, errors, prompts and secrets.
--db-urlis removed fromgen,introspect,doctorandseed, because a connection string holds the password and arguments end up in shell history and the process list. Set$DATABASE_URLorsource.dbUrl, or pipe the URL in with--db-url-stdin.--jsonis a global option. It prints one JSON document on stdout: the command's result, or RFC 9457 Problem Details with acodewhen it fails.doctor --format jsonbecomesdoctor --json.--yes(-y) is a global option. Prompts never run under--json,--yesorCI.- Exit codes are 0 for success, 1 for a failure and 2 for a usage, config or environment error. Every error code has a section on the errors page.
- A mistyped command gets a suggestion:
Did you mean "gen"?. better-supabase.config.*loads through c12 and is checked against a schema; an unknown key or a wrong value stops the run with each problem's path.DATABASE_URLandSUPABASE_API_URLmust be URLs.run()no longer readsprocess.env: passrun(argv, { env: process.env }).CliIo.envandCliIo.noware removed, andCliIo.stdinis new.- New exports:
CliError,CLI_ERRORS_URL,commandNames, and theCliEnv,CliProblemandCliErrorCodetypes. A command can throwCliErrorand returndatafor--json.
-
The CLI moved to its own package,
@better-supabase/cli, released at the same version asbetter-supabase. Install it withpnpm add -D @better-supabase/cli; thebetter-supabasecommand and its options are unchanged.Breaking for
better-supabase: the package no longer ships thebetter-supabasebin or thebetter-supabase/clisubpath. Importrun,registerCommandand the introspection helpers from@better-supabase/cliinstead. The newbetter-supabase/sqlsubpath exports the SQL kit (renderKit,kitLayout,compileReadSets), andbetter-supabase/confignow exports the snapshot types,DEFAULT_CLAIMSandtenantClaimPaths.The library no longer has
@supabase/configas an optional peer; only the CLI readssupabase/config.toml. -
Doctor BS213 warns when
anonorauthenticatedmay insert or update a column that RLS helpers read to decide access, such asmemberships.roleorcontacts.customer_id, and a policy lets them write the row. It also checks thedecidingColumnsin PermDock's manifest and names PD028. The finding lists the revoke and the grant of the remaining columns. Snapshots now record column-level insert and update grants (columnGrants); older snapshots only show table grants. -
Doctor treats
security definerfunctions and functions with asetoption as not inlinable (BS205, BS206), and its advice for them now points tocolumn in (select helper()). New checks:auth.role()(BS109), update policies without a select policy orwith check(BS110), needless and unused API grants (BS111), exposed security definer functions that never check the caller (BS112), storage inserts without the upsert policies (BS113), helpers that readuser_metadata(BS114), zero-argument helpers called withoutselect(BS215), foreign keys without an index (BS216, replacing splinter's lint for the same table), tenant foreign keys that can cross tenants (BS217), soft-delete tables without a partial index (BS218), containment filters without GIN (BS219), column types to avoid (BS220) and direct connections in serverless apps (BS221). BS211 also reportsidle_in_transaction_session_timeout.Snapshots now record each index's access method and predicate, and who among
anonandauthenticatedmay execute the functions doctor reads. Both fields are optional insnapshot-v2.json, so older snapshots still load. -
The
entitlementsSQL kit module reads memberships from PermDock whenpermdock.manifest.jsonis present:has_entitlementchecks<rls.schema>.member_<scope>_ids(),feature_claimsreadsmember_<scope>_ids_for(user_id)from PermDock's hook, andentitlement_membersreads the manifest's membership tables.sql add entitlementsthen no longer adds thetenantmodule or prints the hook note. The scope defaults toorganization; setentitlements.permdock: { scope }for another of the manifest's scopes, orentitlements.permdock: falseto keepbetter_supabase.memberships. The new doctor check BS408 warns when the manifest or the database lacks one of the two helpers. -
Cursor pagination no longer skips rows whose sort column is null: the next page follows where Postgres places nulls (last for
asc, first fordesc, or thenullsoption). On thebetter-supabase/postgresexecutor a cursor over columns that sort the same way and are not nullable compiles to a row comparison such as(name, id) > ($1, $2), which one index range scan can serve.defineListQuery,defineResource,createOpenApiand the MCP table tools acceptpagination: "cursor". The list then takesafterinstead ofpage, returnsnextCursorandhasMore, keepssizecapped bymaxPageSize, and documents the cursor in its OpenAPI parameters and JSON Schema.paginateand offset lists work as before. -
The
audit,webhook-inboxandjobsSQL kit modules addpurge_audit_log,purge_webhooksandpurge_job_archive. Each deletes rows older than an interval in batches and returns how many it deleted, for nightly pg_cron jobs. Onlyservice_rolecan execute them. Runbetter-supabase sql addagain to update the kit files. -
Security fixes in the SQL kit. Rerun
better-supabase sql addfor the modules you installed to apply them:create_invitationno longer lets an admin invite someone asowner; only an owner or the service role can. The service-role check readsauth.jwt() ->> 'role'instead of the deprecatedauth.role(), andinvitations.rolegets the same check constraint asmemberships.role.accept_invitationrequires the signed-in user's address inauth.usersto be confirmed, not only anemailclaim. An unconfirmed user gets the hintINVITATION_EMAIL_UNCONFIRMED.anoncan no longer callhas_org_role, and the pgTAP kit'stests.create_useris granted toauthenticatedandservice_roleonly.- The tenant module adds
member_org_ids(roles), a set-returning helper. Write policies asorg_id in (select better_supabase.member_org_ids()), which Postgres evaluates once per statement instead of once per row. - Deduplicated jobs get a partial index on
dedupe_keyin each queue table, soenqueue_jobno longer scans the queue while it holds the advisory lock. triggerSqlputs the realtime trigger function in thebetter_supabaseschema, which the Data API does not expose, instead ofpublic.
better-supabase gennames a composite foreign key that repeats a column on both sides, such as(customer_id, organization_id)referencing(id, organization_id), after the remaining column: the relation iscustomer, as it would be for a plaincustomer_idkey. Schemas with such keys get new relation names; settables.<name>.relationsto keep the old ones. -
createMcptakesallowedHosts, which rejects requests whoseHostheader names another host (DNS rebinding protection, next toallowedOrigins), andresourceDocumentation, published as RFC 9728resource_documentationin the protected resource metadata. -
next.cached()takestagsandlife.stale.tagsadds cache tags to the entry next tobs:session:<user id>, andlife.stalecaps the stale timesessionStalecomputes, sonext.cached({ tags: [snapshotTag(sub)], life: cacheLifeFor(snapshot) })caches a PermDock snapshot no longer than the token or the snapshot.next.invalidateSession(userId, { tags })also expires the extra tags. PermDock can now drop the "Planned in better-supabase" note in itsguides/next-cache-components.mdx. -
Doctor and
genread PermDock'spermdock.manifest.jsonandpermissions.catalog.json(paths set bypermdock.manifestandpermdock.catalogin the config). BS407 recognizes PermDock's hook by the manifest or its-- permdock:hook v1marker, treats functions insupabase.hook.claimsas the hook's own sources, flags a wrapper that writes those claims again, and reports an info finding whenpermdock.config.tshas no manifest. BS405 takes PermDock's budget from the manifest. The new BS214 reports bucket and topic policies that pass a permission withrowConditions: trueto PermDock's helpers,genrefuses to write such bucket policies, anddefineBucketanddefineTopicthrow for those keys when given the catalog ascatalog. The MCP recipe narrowstool.metawith PermDock'sisPermissionbeforecanandmayUse. -
Support the Supabase CLI's pg-delta engine and native local stack. When
supabase/config.tomlsets[experimental.pgdelta] enabled = true,better-supabase sql addand doctor (BS304, BS305, BS404 and--fix-grants) namesupabase db schema declarative syncinstead ofsupabase db diff, BS404 asks for the grants in the schema file that defines the hook, and the declarative schema files are read fromdeclarative_schema_pathin name order, since pg-delta ignoresschema_paths.better-supabase envfalls back tosupabase status --envwhen[experimental] stack = truemakessupabase status -o jsonfail. -
createPostgresapplies Supabase's role timeouts per transaction:statement_timeout8 s forasUserand 3 s foranon, becauseset roleskips the role's own setting.statementTimeouttakes a number for every role or one value per role (admin,authenticated,anon). New options:idleInTransactionTimeout,connectionTimeoutandidleTimeout(both default to 10 s). Each transaction now sets its timeouts, claims and role in one query afterbegin. -
createPostgres({ pool })runs on an existingpg.Poolor any object withconnect()andend(), typed as the newPgPoolandPgPoolClientexports ofbetter-supabase/postgres.list.parse()now reads a typedListQueryInputsuch as{ page: 2, size: 10 }instead of treating it as URL search params and falling back to page 1, and a non-stringqsuch as{ q: 5 }reportsMust be textinstead of being accepted.contextFromSupabasethrows aTypeErrorfor an auth mode it does not know instead of returning it as the request context. The standards page lists the conformance test behind each adopted standard. -
Doctor reads the declarative schemas in
[db.migrations] schema_pathsorder, then the files no entry matches, then the migrations newest first, and the built-inconfig.tomlparser reads arrays over several lines.sql addnames the kit files noschema_pathsentry matches, becausesupabase db diffwould skip them.doctor --fix-grantsprints the grant and revoke SQL BS404 asks for as one block to append to the migrationsupabase db diffwrote. For PermDock's hook, BS404 points topermdock supabase hook generate --grants-outinstead, and when a-- permdock:grants v1migration already grants the function, it says to apply the migrations. -
Breaking: time values in the public API are now
Temporalvalues instead ofDateobjects and epoch numbers.- The
codecs.timestamptzoption takes'instant'instead of'date'.timestamptzcolumns decode toTemporal.Instant,timestampcolumns toTemporal.PlainDateTime, and both keep microseconds. Rename the option and runbetter-supabase gen. - The
nowoptions ofdefineSupabase, plugin hooks,toCloudEvents,forwardMutations,verifyWebhookandbucket.sweepreturn aTemporal.Instant. Job.enqueuedAt,Job.visibleUntil,InboxMessage.receivedAt,EnqueueOptions.runAt, the verified webhooktimestampand thesignWebhooktimestampareTemporal.Instant.bucket.sweep({ olderThan })takes aTemporal.Durationor aTemporal.Instantinstead of milliseconds.- TypeScript 5.9 is no longer supported, because it has no Temporal lib. Use TypeScript 6 or 7.
On Node 24, Safari and other runtimes without a native
Temporal, installtemporal-polyfilland importtemporal-polyfill/globalonce at startup. Without it, the calls that needTemporalreturn aDbErrorthat names the import. Auth keeps itsnowoption in epoch milliseconds. - The
-
The PermDock docs and the plugins skill reference match current PermDock. With
supabase.hook.claims: { features: 'better_supabase.feature_claims' }inpermdock.config.ts, PermDock's hook writes thefeaturesclaim, sohasEntitlement(session, ...)works next to PermDock. The MCP recipe answersvisiblewith PermDock'smayUse, and the Storage and Realtime pages point to the catalog'srowConditionsandpermdock doctorPD037 for permissions the helpers don't fully check. -
Two public types no longer rely on DOM-only globals. The
headersoption ofproblem()takes what the globalHeadersconstructor accepts, and the JSON Web Keys inbetter-supabase/testinguse thenode:cryptotype, so both resolve in a project whoselibleaves out"DOM". -
The
better-supabaseskill's schema workflow now asks for RLS, policies, policy indexes and Data API grants on every new table, and points to Supabase's own skills. The CLI asks for an access token scoped to the project when it reads a hosted project. -
better-supabase genuses@supabase/postgrest-typegen0.3.1, which still produces the samedatabase.types.tsassupabase gen typesfrom Supabase CLI 2.119. Version 0.4.0 adds aComputedFieldskey that the Supabase CLI does not emit yet, so it waits until the CLI catches up. The optional@supabase/configpeer now accepts 0.11.
0.2.0 (2026-09-30)
-
createMcptakes optional per-tool authorization hooks.authorize(ctx, tool, args)runs after the arguments are validated and beforerun, and returns{ allowed: true }or{ allowed: false, reason?, scopes? }. A refusal is a tool error, and a refusal withscopesis a 403insufficient_scopechallenge that lists the scopes the call needs.visible(ctx, tool)filterstools/list, and a hidden tool is called like an unknown one.defineToolandmcp.toolaccept an opaquemeta, for example a PermDock permission, which both hooks receive and clients never see. New types:ToolRefandToolDecision. The PermDock guide has a recipe that checks PermDock permissions through a structural type. -
Align with PermDock's authorization contract, so a project can use better-supabase and PermDock on the same claims. The migration guide at https://bettersupabase.com/docs/migration/0.1-to-0.2 lists every step.
Breaking changes:
- The active tenant claim is
tenant_idinstead oforg_id, in the token and inapp_metadata. The newclaimsblock inbetter-supabase.config.ts(claims: { tenant, scope, features }) renames it for the tenant plugin, the SQL kit, storage and realtime at once.plugins.tenant.claimis removed. - SQL kit:
better_supabase.current_org_id()is nowcurrent_tenant_id().membership_claims()moves to the tenant module and returns PermDock's[{ scope, id, roles }]. Plan features move out of memberships intofeature_claims()in the entitlements module, which fills thefeaturesclaim ({ [tenantId]: string[] }). MembershipClaimhas PermDock's shape (scope,id,roles, and optionalwithin,via,expiresAt).hasEntitlementandEntitlementKeyread thefeaturesclaim.asUsersigns ES256 tokens with the key frombetter-supabase keys(signing_keys_pathinsupabase/config.toml), like a hosted project.{ alg: 'HS256' }keeps the shared secret and only works against a local stack.SPEC_PINS.mcpandMCP_PROTOCOL_VERSIONare2026-07-28.createMcpserves stateless requests (server/discover, request_meta,Mcp-MethodandMcp-Nameheaders) and still answersinitializefor2025-11-25,2025-06-18and2025-03-26clients. A 403 carries aninsufficient_scopechallenge.- BS405 measures the whole token against 2 KB and, when the project has a
permdock.config.ts,membershipsplusattrsagainst PermDock's 1 KB budget (doctor.claimsLimitoverrides the budget, or the whole-token limit without PermDock).
New:
sb.userMetadata(schema)parsesuser_metadatainto a typedsession.profilefor display. Metadata that fails the schema leavesprofileundefined and logs one warning with the failing paths. No authorization code reads it.ProfileOf<B>inbetter-supabase/reacttypesuseSessionfromcreateHooks.- Buckets take a PermDock policy (
{ permdock: { read, write }, scope }) and topics apermdock: { receive, send, scope }option. The generated policies check PermDock permissions through itspermitted_<scope>_ids()orpermdock_has()helpers instead of the tenant claim. sql add tenantstops when apermdock.config.tsexists, unless you pass--force. Doctor BS407 reports a hook that callsmembership_claimsor writes PermDock's claims next to PermDock.better-supabase/testingexportssignLocalJwt,localSigningKey,signTestJwtWithKeyand theSigningJwktype.- A new docs page, https://bettersupabase.com/docs/auth/permdock, shows both packages in one project.
- The active tenant claim is
-
Doctor BS405 now measures what PermDock's budget covers. With a
permdock.config.ts, it warns whenmembershipsplusattrsfrom the hook's output pass 1024 bytes (ordoctor.claimsLimit), measured withoctet_lengthas PermDock's hook measuressupabase.hook.budget. The whole-token check at 2048 bytes is a separate warning, so a normal PermDock token of about 1.5 KB no longer trips it. Without PermDock,doctor.claimsLimitstill limits the whole token.memberships_truncatedis reported as before. -
Entitlements are no longer treated as a PermDock conflict, because
featuresis not a PermDock claim.- Doctor BS407 reports a custom access token hook next to PermDock only when it calls
better_supabase.membership_claimsor writesroles,user_role,membershipsor the configured tenant claim itself. It no longer reportsfeature_claims, and it leaves PermDock's generated hook alone. sql add entitlementsworks next to apermdock.config.tswithout--force. It writestenantas a dependency and prints a note. Onlysql add tenantstill needs--force.- The PermDock and entitlements docs say what works today.
has_entitlement(tenant, key)andtenant_entitlements(tenant)work in SQL and RLS without a claim.hasEntitlement(session, ...)needs thefeaturesclaim, which PermDock's hook doesn't write yet, so read entitlements from the database on the server until PermDock adds a hook slot for it.
- Doctor BS407 reports a custom access token hook next to PermDock only when it calls
-
The PermDock guide lists every claim PermDock's generated hook writes (
user_role,roles,memberships, the tenant claim,attrs,authz_verandmemberships_truncated). The migration guide and thesb.userMetadata()TSDoc say thatsession.profileis for display only, and that roles, memberships, the tenant and entitlements never come from it. -
The storage and realtime docs say that the
permdockpolicy mode compares ids as text, so a path or topic segment must be the id's canonical lowercase form: an uppercase uuid is denied. An integration test covers it for buckets and topics. -
The docs now name the right PermDock command for the SQL helpers.
permdock_hasandpermitted_<scope>_idscome frompermdock rls generate, not frompermdock supabase hook generate. The PermDock guide gains a setup step that runs both commands, and says that the helpers live in PermDock'srls.schema, which must equal theschemaoption on buckets and topics. -
Document that the
permdockpolicy mode for buckets and topics only fits permissions whose grants have no row conditions beyond the scope. PermDock's helpers check role and scope, so a permission with row conditions (for exampleownerId = principal.id) would grant every object in the scope. Leave those to the policiespermdock rls generatewrites. The storage, realtime and PermDock docs pages show a wrong and a right example, and the TSDoc on thepolicyandpermdockoptions says the same. -
The PermDock guide says that a non-default
claims.tenantmust also be set as PermDock'srls.tenantClaimand passed tosubjectFromSupabaseorsubjectFromSupabaseSessionas{ tenant }, so both packages read and write the same tenant claim. -
The docs site answers questions with Ask AI, shows the last-updated date on every page, and has Open Graph images, a sitemap, type tables generated from the source, and links that add the docs MCP server to Cursor and VS Code. The repository now has a root
CHANGELOG.md, and its fixture schema is declarative, with pgTAP tests for RLS. The published package does not change.
Every release of better-supabase, newest first. pnpm version-packages adds a
section from the pending changesets. Releases up to 0.1.0 are listed in
packages/better-supabase/CHANGELOG.md.
Last updated on