Repository navigation
v0.26.0
Breaking
Server & REST
-
A create or update naming a multi-hop via relation is refused with a 400.
Aviachain longer than one link (a to-many chain with no single junction
table, or a to-one chain of two or more steps) failed inside the transaction
as500 RELATION_MISCONFIGURED, telling you to runrebase schema generate;
a two-step to-one chain was written by re-pointing the target's key at the
first step's column. Both are now refused up front with
400 RELATION_NOT_WRITABLE, naming the field. Single-hopviawrites (to-one
in one step, to-many through one junction table) work as before, the generated
SDK now types them, and the admin form disables the picker of a multi-hop
via. -
Role names with a comma, a brace, a double quote, a backslash or a control
character are refused. Users, the users collection and service keys answer
400 INVALID_ROLEfor one, and for an empty name. Roles are now stored as
bound parameters (see Security). -
Local storage listing page tokens have a new format (
n.…). A token
issued before the upgrade is refused with400 INVALID_LIST_OPTIONS; start
the listing over. -
The functions upstream must be an http(s) URL, and a forwarder needs the
service key. AREBASE_FUNCTIONS_UPSTREAMthat is nothttp://or
https://, or one set withoutREBASE_SERVICE_KEY, now stops the boot with
a message naming the variable. Before, the boot
succeeded, every forwarded call failed with 502, and without the key every
in-processrebase.functions.invokeanswered 401. -
An OAuth sign-in no longer refuses an unverified account with a provider
that verified the email. The reasonslocal-account-unverifiedand
local-account-unverified-passwordlessare gone: the sign-in now verifies
the matching unverified account, and removes its unproven password,
identities and sessions first (see Security). A client that branched on
those reasons never sees them again.
MCP
- The MCP server no longer accepts the CLI's control-plane token as the
app's API key.@rebasepro/mcpignoresREBASE_TOKENfor the app. Set
REBASE_API_TOKENfor the app;REBASE_TOKENis the CLI's control-plane
token and stays the CLI's (see Security). Until you do, the MCP server stops
authenticating with the app and says so once on stderr.
Client SDK
- The generated types drop the foreign key of a to-one relation the API
closes. For anexcludeFromApibelongsTo,Row,InsertandUpdateno
longer carry it, and awrite: []relation no longer offers its key on
writes, matching what the server already refused (see Fixed). Code that read
or wrote that key stops compiling afterrebase generate-sdk.
CLI & Cloud
-
The cloud webhooks command is retired.
rebase cloud webhooksnever
delivered anything. Every action now exits 1 with codecommand_retiredand
namesrebase cloud notifications, and aWebhookDispatcherin app code for
row changes. -
Asking a cloud backend to scale to zero is refused.
rebase cloud compute set --scale-to-zero truenever did anything; use
--replicasand--autoscale-max.falseis ignored with a warning (see
Fixed). -
Cloud tokens may need creating again. A
--can deploy,--can envor
--can logstoken created before this release does not cover previews, and
reading the audit log with a token now needs--can audit(see Added).
Added
Studio
- Review a database branch like a pull request, and merge it. Picking a
branch in Studio's Branches pane shows what it changed against the database it
was made from: its tables, columns, constraints, indexes, triggers, enum
labels, views, functions and RLS policies as SQL, side by side, and per table
the rows it inserted, updated and deleted, with samples. Rows the parent
changed on its own meanwhile are counted apart, and rows both sides changed
are conflicts — each row's transaction id says which side wrote it. Merge
writes the branch's rows to the parent in one statement, all or none: the
parent's own changes are kept, a conflict writes nothing unless you choose
which side wins, and a diff that went stale writes nothing. A row the branch
deleted that newer parent rows still reference is a conflict as well
(referencedByon the diff), so a merge never removes them through a foreign
key unless you choose the branch's side, and then itswarningslist what
ON DELETEreached. A table that cannot be matched row by row, or was
rewritten since the fork (TABLE_REWRITTEN), blocks the merge. A merge the
parent refuses answers409 BRANCH_MERGE_FAILEDnaming the table and
constraint. Schema and policies are never merged. Reading the diff needs
database:write, like the merge, because its samples are rows of both
databases read outside row-level security; samples are capped at 1,000 rows a
table, and a column named like a credential (password_hash,*_token,
*secret*) shows[redacted]. The same diff isrebase db branch diff,
GET /api/admin/branches/:name/diff, the read-only MCP tool
rebase_db_branch_diff, anddiffBranchon the driver's admin; see
Reviewing and merging a
branch. The fork
point is recorded when the branch is created, so aGRANTor
ALTER DATABASEon the branch no longer makes the merge count the parent's
later rows as branch deletes; a branch created before this release is still
placed by its catalog row.
Admin (CMS & app)
-
Customization recipes ship with
@rebasepro/cms.node_modules/@rebasepro/cms/recipes
holds tested, copy-ready examples: a site-settings singleton, per-user
preferences, a bulk "set status" toolbar action, a calendar view mode, a
dashboard page and a custom login screen. They import only package entry
points, each has a test in the repository, and theirREADME.mdis the
reference of building blocks, hooks and places an agent reads before
customizing. The docs page is Customizing the Admin,
and therebase-customize-adminagent skill points at the recipes. -
EntityFormPanelcreates a record under the id it is given when no record
has it yet. A record kept at a known key — a settings singleton, a profile
keyed by its user's uid — said "not found" until something seeded it; now the
first save creates it there. Where the collection marks a key propertyisId,
the id is that property's value, set and locked. The form is disabled where
creating is not allowed, including while access is simulated. -
The leaf parts of two overridable components are exported, so a copy of
the component compiles with package imports:NavigationCardand
SmallNavigationCard(HomePage.CollectionCard), and
StringNumberFilterField,BooleanFilterField,DateTimeFilterField,
ReferenceFilterField,RelationFilterField,getDatePropertyModeand
getDatePropertyTimezone(Collection.FilterField).
pnpm report:override-copyabilitylists what still keeps each overridable
part from being copied that way. -
16 of the 19 overridable parts can be copied into a project and compile
with package imports, up from 5. What a copy reuses is exported: -
drawer:
DrawerExpansionContext,useDrawerExpansion,
useDrawerExpandedWhile,DrawerExpansion,NAVIGATION_DEFAULT_GROUP_NAME; -
collection toolbar and table:
CollectionTableToolbar,
CollectionTableToolbarProps,useToolbarLabels,EditorCollectionAction,
CollectionTableBindingProps,EntityTableCell,EntityTableCellProps,
EntityTableCellOpener,PropertyTableCell,PropertyTableCellProps,
AdditionalFieldValue,AdditionalFieldValueProps,propertiesToColumns,
PropertiesToColumnsParams,CustomFieldValidator; -
cards and previews:
resolveCollectionSlotKeys,useCollectionSlotKeys,
useEntitySlots,CollectionSlotKeys,EntityPreviewSlots,PreviewSlot,
DatePreviewSlot,RelationChipSlot,RelationChipItem,SlotValue,
SlotValueProps,TagChips,useEntityTitle,UseEntityDisplayParams,
ResolvedDisplayValue,useSearchExplanation,MatchExplanation,
fieldLabel,Highlighted,NestedEntityPreviewBoundary,
EntityPreviewLayout,isEmptyPreviewValue; -
forms and records:
getParentLinkValues,getTouchedPropertyValues,
getUnappliedLocalChanges,overlayEdit,unlinkEntityAction,
entityActionName,resolveEntityActionState,EntityActionState,
useSideDialogContext,SideDialogController,useChildViewSource,
EditViewBinding,EditViewBindingProps,DetailViewBinding,
DetailViewBindingProps; -
home page:
NavigationCardBinding,NavigationGroup,FavouritesView; -
login screen (
@rebasepro/app):startOAuthRedirect,
consumeOAuthCallback,OAuthRedirectRequest,OAuthCallbackResult,
readMfaRequired,canAnswerMfa,classifyMfaRefusal,PendingMfaSignIn,
authErrorMessage,appAddressOfEmailLink,emailChangeErrorMessage.Collection.View,DetailViewandHomePage's drag-and-drop stay
uncopyable on purpose; the recipes README says why and what to customize
instead. A test fails when a part that copies stops copying. -
"Simulate access" in the app bar: see every collection as a role, a user or
a service key. Pick one and every data view reloads showing only the rows
and fields it can read, under a banner that counts the collections it cannot
see and the columns masked from it. Simulation is read-only: the form, Revert
and every other edit are off while it is on, and a record's History tab and
its "last edited by" are read as the simulated identity too.
client.simulate()is the client behind it, typed by yourDatabaseand
closable. -
escapeCsvFormulaandunescapeCsvFormulaare exported from
@rebasepro/utils, so code outside the admin writes CSV with the same
formula escaping.@rebasepro/cmsstill exports both.
Server
- The RLS simulator: an administrator's data request can run as a role, a user
or a service key.x-rebase-simulate-role,x-rebase-simulate-user-idand
x-rebase-simulate-api-key-idon the data API and custom functions run the
request through the real policies as that identity (customerholding uid
u_1need not be an account), and a key's scopes apply as they would to the
key. Simulation is read-only: a write, delete, batch, history revert or
non-GET function call carrying the headers gets403 SIMULATION_READ_ONLY.
Simulatinganonsimulates no session, so a function'srequireAuthrefuses
it, and a simulated request spends the administrator's rate limit, not the
simulated key's. The response'sx-rebase-simulated-contextnames the role,
the uid and the fields that identity's roles cannot read. Anyone but an
administrator's own session gets403 SIMULATION_FORBIDDEN, and every other
route refuses the headers./api/admin/rls-simulatorlists what can be
simulated and previews each collection's visible rows and masked fields. REBASE_PROVISION: set it tofalseand Rebase changes nothing in your
database. For a database whose schema changes only through your own change
process. No table, column, index, function, trigger, policy, role or grant is
created or altered, at boot or later. That includes the framework's own
tables, whichREBASE_MIGRATE_ON_BOOT=nonestill keeps current, and the
tables the runtime used to create on first use: idempotency keys, cron claims
and the shared rate-limit counters. The runtime reads what is there and names
what is missing. Missing auth tables, or a request role that can reach
rebase.refresh_tokensorrebase.api_keys, stop the boot, with theREVOKE
to run. A missing optional table turns its feature off and says so. The schema
comes from a provisioning run your process controls:
REBASE_PROVISION_ONLY=truewithREBASE_PROVISION_SQL_OUT=<file>writes
every schema change the run made as a SQL script to review and apply with
psql. That run no longer starts cron or the job queue, and now creates the
first-use tables too.initializeRebaseBackendreads the variable as well; a
server built withcreateRebaseBackendpassesprovision: false. Studio's
schema change dialog then commits a change as source only, for your process to
apply. See When only your change process may change the
database.
Creating a database branch on such a server names the missing
rebase.branchesregistry and how to create it.openRebase({ callbacks })runs the project's global callbacks on a
script's writes, as the server does for an API write.rebase console
passes the project's own, fromconfig/index.ts; when that file cannot be
imported it says so in one line and opens without them.- Optional driver hooks for whole-store sweeps:
DataDriver.scanStoredRows,
StorageController.storageLocationand the history service's
scanStoredValues.rebase storage orphansreads rows through them as
stored, past any read hook. - For realtime adapter authors:
RealtimeSocketOptions.runInScopeand
RealtimeProvider.setScopeRunner, so a hook reached over your transport
sees its own backend asrebase.devEmailSinkFor(sendEmail)in
@rebasepro/server's email exports gives a backend its own development
mailbox. - A worker process forwards its own function calls to the functions
process. WithREBASE_FUNCTIONS_UPSTREAMset on aworker, its in-process
rebase.functionscalls go there. Its port still serves nothing under
/api/functions. It needs the deployment's
REBASE_SERVICE_KEY, like theapi. - On worker and functions processes, account administration and the cron
controls work from server code.rebase.admin(users, roles, password
resets) andrebase.cronare carried in-process for that process's own jobs
and handlers. Before, they answered
rebase.authstill answers a 404 that names the surface and points at
rebase.admin.
- Collection callbacks name the API key a write was made with, as
context.credential:{ kind: "personal" | "service", id, name }for a
write through the data API, the realtime socket or/mcpwith anrk_key,
so a personal key's write can be told apart from its owner's own. It is
absent for a session, the service key, server-side work and the browser. New
typeCallerCredential;User.credentialandRebaseCallContext.credential
carry it, and the socket'sresolveApiKeymay return the key'snameand
kind.
CLI
-
rebase cloud deploychecks a static app's JavaScript before uploading
it. A static app whosebuildonly copied hand-written scripts into its
output deployed a file that did not parse. The deploy succeeded, and the live
page failed on load. Every.js,.mjsand.cjsfile in each static app's
output is now parsed, with the inline<script>blocks of its HTML, before
anything is uploaded. That covers static apps deployed on their own and those
folded into the backend's bundle. A syntax error stops the deploy with
file:line:columnand the offending line (--jsoncodesyntax_error).
--no-verifyskips the check. The parser is acorn, now a CLI dependency. -
A successful deploy says how to undo it. It ends with
"rebase cloud rollbackputs the previous deployment back." A static app
deployed on its own records no bundle, so it cannot be rolled back yet; its
deploy says so and suggests deploying the last good version again. -
rebase cloud deploy --previewserves a build at its own address and leaves
production alone. Before, the only way to see a change on the platform was
to ship it to production. The new flag prints an address like
https://<project>--<name>.rebase.website, and--preview-namekeeps the
same address across deploys. A preview runs its own backend, on an empty
database of its own. Rebase Cloud does not serve a static app's own preview
yet, sorebase cloud deploy <app> --previewfor a static app is refused with
PREVIEW_STATIC_UNSERVEDand publishes nothing. The deploy checks that the
preview's address answers before calling it live, as a production deploy does.
--can deploy,--can envand--can logstokens cover previews; a token
created before this release must be created again to use them.
rebase cloud previewslists and deletes previews. A preview is started
through its own control-plane function, never throughdeploy, so a control
plane without previews refuses it withpreviews_unavailableand nothing
ships to production. The control plane's half is specified in
docs/preview-deployments.md. A backend preview's deploy says what it costs
per month, from the control plane's own quote (costunder--json), or
gives the billing rule and a docs link when the control plane cannot price
it. Rebase Cloud pauses a backend preview whose database grows past the
preview limit (2 GB by default);rebase cloud previewsprints why under a
paused preview (statusMessagein--json), and
rebase cloud notifications add --events preview.pausedsubscribes to it. -
Previews can run with their own environment variables. A backend preview
starts from production's variables, and that copy carries production's live
third-party keys, so a preview that charges a card does it for real.
rebase cloud env set KEY=VALUE --previewgives previews their own value (a
test key),env unset KEY --previewleaves a variable out of them,env reset KEY --previewgives them production's value again, andenv list --preview
shows what differs. None of them changes production. A control plane without
previews refuses them withpreviews_unavailable. -
Private npm packages install on the managed runtime. A managed pod
installs a bundle's dependencies when it starts, from the public registry
only, so a package under a private scope or registry never installed.
rebase buildnow copies the project's.npmrc(andbackend/.npmrc) into
the bundle. Every credential in it must be an environment reference such as
${NPM_TOKEN}, set withrebase cloud env set NPM_TOKEN=… --secret; a
literal token fails the build, because the bundle is uploaded with the code. -
rebase cloud login --codesigns in with a 6-digit code emailed to you,
for accounts made with Google, GitHub or LinkedIn, which have no password. At
a terminal,rebase cloud loginoffers "Password" or "Email me a code" when
the control plane sends codes. With two-factor authentication on, either path
asks for the authenticator or recovery code; without a terminal it exits
mfa_requiredand names access tokens. A refused password now names--code
and access tokens (REBASE_TOKEN). -
rebase cloud activityprints the organization's audit log: deploys,
rollbacks, env var changes (names only), domains, backups, point-in-time
restores, database password reveals, project and member changes and access
tokens, each with who and when.--project,--action,--actor,--limit,
--beforeand--orgfilter it, and--jsonworks. Owners and admins can
read it, here and on the console's Activity page. -
rebase cloud orgs members inviteinvites anyone to an organization by
email (--role admin|member). Everyone, an existing account included, gets
a single-use link, valid 7 days, that only an account which verified the
invited address can accept; nobody joins an organization without accepting.
--role owneris refused withinvite_role_not_allowed: invite as admin,
then promote withorgs members role.orgs members remove,
orgs members roleandrebase cloud orgs invites(list, revoke, resend;
owners and admins only)
manage the rest, andorgs membersshows emails. -
rebase cloud billing invoicesandrebase cloud billing upcominglist
the organization's issued invoices with Stripe's hosted page, and preview each
subscription's next invoice. Owner or admin only;--jsonworks. -
rebase cloud usageprints this month's requests against the included
allowance, the overage so far and the projected month-end figure, for the
linked project or, withorg, every project in the organization.
--month YYYY-MMpicks another month,--jsonworks, and the figures carry
their known accuracy limits ascaveats. -
rebase cloud logs --runtime -ffollows the running app's log from every
pod until Ctrl-C. Each line shows once, pods that start, stop or restart are
announced, a restarted container's last lines are printed and dropped
connections are retried.--runtimealso takes--since 5m|<RFC3339>,
--tail <n>,--grep <text|/re/>,--leveland--previous. With--json
a follow prints NDJSON, and a snapshot addspods,state,message,
truncated,previewandnextSinceTimebesidelogs.
rebase cloud logs --preview [name]prints or follows a preview's build log,
and with--runtimeits backend's log. -
rebase cloud notificationssends a project's deploy, backup, billing and
usage events to Slack, Discord or any https URL, signed with
X-Webhook-Signature:
add <url> [--events …] [--format json|slack] [--name …],list,test <id>
andremove <id>. -
rebase cloud claimprints the claim link of the project
rebase cloud init --claimprovisioned here, with the time left (--open
opens it), and refuses withalready_claimedonce someone has claimed it. -
rebase cloud env set KEY --stdinreads the value from standard input,
keeping it out ofpsand shell history. -
rebase storage orphansscans in the background on the backend and shows
its progress, so a project too big to scan inside one request (an ingress
timeout) finishes. NewPOST /api/admin/storage/orphans/scansand
GET /api/admin/storage/orphans/scans/:id. With the job queue on, the scan
is astorage.orphansjob; without it, the server runs it in its own process
and says so. A scan is one attempt bounded byjobs.timeoutMs. -
rebase storage movelists the source again after the move and names
every file it did not move (exit 1;--jsonnotMoved). New
POST /api/admin/storage/move/verify. -
rebase cloud app-keyshows the app key a claim hands over, and
rebase cloud app-key rotatereplaces it. Claiming a project
init --claimprovisioned now removes the expiry of theREBASE_API_TOKEN
it wrote into.env, which used to end with the claim window, and the notes that said the key stops
working even once the project is claimed now say a claim hands it over.
app-keylists the app's agent keys by prefix, never the value.rotate
(confirm or--yes) mints a key that does not expire, revokes the earlier
agent keys, and writes the new one into a.envthat already sets
REBASE_API_TOKEN(keeping it out of--jsonthere) or prints it once.
Signed in only. -
rebase cloud service-keyshows a project's service key generation, and
rebase cloud service-key rotatereplacesREBASE_SERVICE_KEY. The app
restarts onto the new key, and the old one stops working once the restart has
finished;service-keysays whether the restart is still under way and never
prints the key (rebase cloud env reveal REBASE_SERVICE_KEYdoes). Claiming a
project rotates it as well, so nothing an agent's code read before the claim
stays valid. Signed in only. -
rebase cloud tokens create --cantakes organization capabilities:
billing,auditandnotificationsgive a token one organization's
billing, audit log or notifications (org:<capability>:<organizationId>; the
organization is--org, else the project's own, else the one
rebase cloud useselected). A token may hold only organization
capabilities.tokens list,whoamiandtokens create --jsonread them
back. Reading the audit log with a token now needsaudit; a token from
beforebillingexisted keeps billing access for one release, and the CLI
prints the control plane's deprecation notice once, on stderr.logsnow
also coversrebase cloud usage. -
rebase cloud clusters registrygives a customer-owned cluster an image
registry of its own (platform admins):--repository,
--push-service-accountand--pull-key-file, all three or--clear. That
cluster's builds push only to its own repository, and its image pull secret
holds only the reader key. A pull key that belongs to the push account is
refused before it leaves the machine, and the key is never printed. A deploy
to a cluster Rebase does not operate that still uses the shared registry says
so in its deploy log. -
rebase verifychecks a project in one fast, offline pass. Every
collection file loads and passes the boot validator, with unknown keys as
errors (a misspelt option, a top-levelwidgetinstead ofadmin.widget).
Every collection declares or inheritssecurityRules, and one with a
user_id,owner_id,organization_idortenant_idcolumn does not lean
on directory defaults. The migrations still build the declared tables and
columns, whichfindUnmigratedSchemafrom@rebasepro/server-postgres
reports by replaying the migration files, with no Atlas and no dev database.
--livealso runs the rls-check catalogue againstDATABASE_URLand checks
that every migration is applied.--jsonnames each finding's file, config
path and fix;--quietprints only problems. -
Scaffolds ship a Claude Code hook that runs
rebase verifyon every
collection edit..claude/settings.jsonand
.claude/hooks/post-edit-collection.shrunrebase verify --quietafter an
edit underconfig/collections/and hand the findings back to the agent.
rebase skills --hooks(orskills install --hooks) installs the same files
into an existing project, merging into a settings file already there. -
rebase generate view <slug>scaffolds a custom admin screen. It writes
<frontend>/src/views/<Name>View.tsx, built with@rebasepro/uiand wired
touseData,useAuthControlleranduseSnackbarController, lists it in
views/index.tsand, on the first run, passes that list to
<RebaseCMS views>. With--collection <slug>it is an entity view instead,
registered in the collection'sadmin.entityViews. AnApp.tsxor a
collection it cannot read safely gets the view file and a printed line to add. -
rebase cloud init --claimgives a project a live Rebase Cloud backend with
no account. It provisions the project, links the directory, deploys the
collections and prints the live URL, the admin panel URL and a 72-hour claim
link. It writesREBASE_BASE_URLandREBASE_API_TOKENinto.env; that
key holdsdata:readanddata:writeonly and expires with the claim
window. The project's own token and claim link stay in
~/.rebase/credentials.json, never in the repository. With--no-deploy, or
after a failed first deploy, the next successfulrebase cloud deploymints
the key. The MCP toolrebase_cloud_provisionruns the same command. -
npx @rebasepro/rls-check --format agentprints a remediation for each
finding. One JSON document names each finding's check id and failing
object, and says whether the fix belongs in SQL or in a collection's
securityRules. It gives the statements, the placeholders it cannot fill,
alternatives and caveats, and the same fix as a
defineCollection({ securityRules: [...] })snippet, or why rules cannot
express it.--jsonis unchanged;--jsonwith--format agentis refused.
Client SDK
client.admin.listUsers({ role, ids })filters by role and resolves a known
set of users in one round trip, sent as?role=and?ids=a,b.
MCP
- Cloud tools past provisioning:
rebase_cloud_status,rebase_cloud_deploy,
rebase_cloud_logs,rebase_cloud_env_list,rebase_cloud_env_set,
rebase_cloud_deployments_listandrebase_cloud_rollback. An agent can
check, deploy, read logs, configure and roll back the project its directory is
linked to without shelling out. They use the CLI's credential and the control
plane's own permissions, env values travel through stdin and are never
returned, and logs are one snapshot. Every cloud tool,
rebase_cloud_provisionincluded, refuses arguments its schema does not
declare.
Helm chart
networkPolicy.ingressNamespacelets the chart's NetworkPolicy admit an
ingress controller other than ingress-nginx, such as k3s's Traefik in
kube-system.
Changed
Admin (CMS & app)
- Form fields are full width unless they are a pair of like compact fields, or
the collection setsadmin.span. The form used to guess widths from
property types — strings, dates and relations at half, numbers and booleans at
a quarter, the title property full — and paired fields that had nothing to do
with each other: Namespace beside Custom Domain, Git Branch beside Git
Repository URL. Now a field takes the whole row unless it and its neighbour
have the same small editor (two numbers, two switches, two dates of the same
mode, two selects with short options) and neither has a long label, long help
text,conditions, a customField, a field-levelaccessrule or
excludeFromApi. Free-text strings are never paired, nothing reads the title
property, and rows are no longer stretched to close a gap. A span written on a
property is used as written. - Fields are filled boxes with the label inside, the way FireCMS draws
them. A field in@rebasepro/uino longer draws a hairline: its fill is
stronger (white 6.5% in dark mode, black 5% in light, from 4% and 3.5%) and is
the whole edge, everywhere aTextField,Select,MultiSelect,
DateTimeField, switch row or search bar is used. The 1px border stays,
transparent, so no control changes height and an error still colours it
without moving the form. In the record form, text, number, date, switch,
single-select and one-line read-only fields carry their label inside a 64px
box, with the value at 16px; a read-only value keeps its outline and no fill.
Multi-selects, uploads, markdown, relations and custom editors keep their
label above a 48px box. The description sits under the box, indented to the
value, and the property's key shows at the end of that line on hover. Rows
are 24px apart. A required field's asterisk is no longer read out as part of
its name.
CLI
- A cloud deploy ends on a summary that says where the app is and whether it
is live.rebase cloud deploylists each app it shipped with its URL, the
deployment id and how long it took, then probes each URL — a backend's
/health(then/api/health, so a non-defaultREBASE_BASE_PATHis
found), a static app's page — for up to--live-timeoutseconds
(default 60,0skips) and says whether the new version answers. Before, a
run through an agent or a pipe printed only{"success":true,…,"hosts":[]},
and people checked every deploy withcurl. The summary now prints when
piped too;--jsonorREBASE_JSON=1gives the object, which gains
project,apps,durationMs,liveandliveChecks. A piped run without
either says on stderr how to get JSON back. Every otherrebase cloud
command still switches to JSON when piped. - A destructive db push approved by its --yes flag now warns.
rebase db push --yeswarns, once per process, when it approves a destructive
change. From the next release--yesapproves only non-destructive pushes;
pass--allow-destructivefor the rest. - Local commands warn when .env points them at the linked cloud project.
rebase api-keys,rebase storageandrebase auth reset-passwordsay so on
stderr when.env'sREBASE_BASE_URLis the address of the cloud project
init --claimlinked; which address wins is unchanged. - An orphan delete removes files only from the scan it just printed.
rebase storage orphans --deletedeletes within 15 minutes of that scan completing, in the background
(DELETE /api/admin/storage/orphans?scan=:id). Every row and history entry
is read again against the candidates, and each file is asked for again right
before it is deleted. A file a row names now, or one uploaded again, is kept
and listed.
Server
- The realtime per-user socket ceiling counts each API key's sockets against
that key, not against the account that made it
(REALTIME_MAX_SOCKETS_PER_USER). - An impersonating administrator's sockets count against the administrator
underREALTIME_MAX_SOCKETS_PER_USER, not against the user they act as. - An application's own
notFoundhandler answers unmatched paths under the
API base path too. Only Hono's default404 Not Foundis turned into the
JSON envelope.
Client SDK
- A typed client created without its collections dictionary says which
accessor was used, where the request went and how to fix it. The one-time
warning now names the accessor and the/data/<slug>path it was sent to,
and gives both fixes: passcollections: collectionsDictionaryfrom
generated/sdk/database.types, or useclient.data.collection("<slug>").
collectionsis documented as required for a typed client.
Deprecated
GET /api/admin/storage/orphans, andDELETEwithout?scan=, the
scan inside one request. They answer withDeprecation: trueand aLinkto
the scan routes, and will be removed in a later release.- The hook that builds an effective-role controller is deprecated.
useBuildEffectiveRoleControlleronly changes which Studio views are listed,
never what a role can read. It still works and warns once, naming Simulate
access: test permissions withuseRlsSimulationorclient.simulate().
<Rebase>no longer calls it for its fallback, so an app that never calls it
sees no warning.
Fixed
Runtime
- Google sign-in works on a Rebase-hosted app. The runtime sent
Referrer-Policy: no-referrer, thesecureHeaders()default, so a page told
other sites nothing about where it came from — and the "Sign in with Google"
button, which checks the page's origin against the OAuth client's authorized
JavaScript origins, answered "The given origin is not allowed for the given
client ID" on every app, however the client was configured. Every response now
carriesstrict-origin-when-cross-origin, the browsers' own default: other
sites see the origin, never the path or query. A response that sets its own
policy keeps it, so the MCP consent page staysno-referrer. An ejected
backend gets the same throughoverridableSecurityHeaders()from
@rebasepro/server. - The MCP consent page goes out with
X-Frame-Options: DENY. It asks for
DENY, butsecureHeaders()runs after the route and overwrote it with its
ownSAMEORIGIN, so a browser that ignores CSP could frame the consent screen
from the same origin. Itsframe-ancestors 'none'still held everywhere CSP
is read.SAMEORIGINis now the default for a response that sets none, the
same wayReferrer-Policyis; an ejected backend turnsxFrameOptionsoff in
itssecureHeaders()and installsoverridableSecurityHeaders(). - The Helm chart's migration Job starts. It ran as
REBASE_ROLE=worker
withREBASE_MIGRATE_ON_BOOT=ensure, and the runtime refuses that pair, so
every defaulthelm installandhelm upgradestopped at its pre-install
hook. Even admitted, aworkerwould have created no collection tables. The
Job now runs asall, with cron and the job workers off, and exits once the
schema is in place, so it also creates the tables each surface owns.
check:chartnow runs every rendered unit's env through the runtime's own
role rules, and fails on a refusal, or on a schema that no unit or two units
would create. - The Helm chart refuses
REBASE_PROVISION_ONLYinconfig.env. The
variable makes a process provision and exit 0 instead of serving, and
config.envreaches every unit. Each Deployment's pods would provision,
exit and be restarted, and the rollout would never become ready. It is now
one of@rebasepro/server'sTOPOLOGY_ENV_VARS, the variables the deployer
decides and the project's own environment may not. The chart's migration Job
still sets it in its ownenv. - The request role no longer gets write access to
rebase.schema_metaon
first boot. On a backend without auth, the collection schema stamp created
that table after boot's last sweep of the request role's grants. So
rebase_userheld DML on it until the next boot, and a user-context query
could rewrite the version stamp that decides whether that boot serves. The
stamp now revokes on the table when it creates it, as every other internal
table's creator does, and runs no DDL at all once the table exists. - A function that imports
honoitself loads on the managed runtime. The
bundler removeshonoand@hono/node-serverfrom a bundle's dependencies
because the image provides them, but nothing linked the image's copy into
the bundle, soimport { Hono } from "hono"failed with "Cannot find
package" and the function's route answered 404. Both are now linked in at
boot, like@rebasepro/serverandzod. - A raw SQL call naming a data source runs on that source's own connection.
rebase.sql(query, { database: analytics })treated the key as a database
name on the default source's server: "database does not exist", or the wrong
database, silently. A key that is no configured source is refused by name, and
openRebase(...).sqlrefuses a source it did not open. - Hooks reached over the realtime socket, a live query's refetch or MCP see
their own backend asrebase, as they do over REST. With several backends
in one process they reached the process default, or threw "server not
initialized yet". - In a split deployment, the api forwards every Set-Cookie a function sets,
not only the last one, so a sign-in that sets a session and a CSRF cookie
works through the api. An unreachable functions upstream answers the standard
error envelope, with arequestId. - A graceful shutdown waits for the work a request handed to
waitUntil
while it was still in flight at SIGTERM. - On a process whose role turns a surface off, the in-process client says
so. On aworkerorfunctionsprocess,rebase.functions,.auth,
.adminand.cronname the surface that is off and what to do, instead of
"No route for …". createRebaseBackendno longer writes the users collection it discovers
back into the caller'sauthobject, and each backend's development mailbox
(/api/admin/dev/emails) serves its own captured mail, not that of whichever
backend registered last.- Impersonating a user or key spends the administrator's rate limit, not the
target's, so a production key'srate_limitis no longer consumed by an
admin acting as it. - Live schema editing from a built bundle refuses a second change while one is
committed and not deployed, also after a restart, on another replica, and
when a developer pushed the change. The guard was a flag in process memory; it
now compares the repository'sschema.generated.tswith what the running
collections generate, and the refusal names the file and the command that
brings it back in step. - Storage orphan deletion no longer deletes files that rows still name.
rebase storage orphans --deleteread rows throughbeforeQueryand
afterReadhooks, so a file a hook hid was called an orphan. The scan now
reads rows as stored, a collection with read hooks on a driver that cannot
read past them blocks the delete (409 STORAGE_SCAN_INCOMPLETE), and a file
only a row's history names is no longer an orphan, so a revert cannot restore
a row pointing at a deleted file. - Moving storage with --delete-source moves every file, and never deletes them
all.rebase storage move --delete-sourcemoved half of a local source per
run, because its page token was an index into a folder the move was shrinking.
Between two sources that store in the same bucket or directory it deleted
every file; that move is now refused with400 STORAGE_SAME_LOCATION. - The OpenAPI create body names every relation the server writes: a
hasOne, the to-many memberships (with amanyToMany's_pivot) and a
single-hopvia, not onlybelongsTo, matching the generated SDK'sInsert. - Remote MCP can find a trashed row to restore.
delete_documentno longer
tells the model that every delete is permanent, andquery_collectionand
count_documentstakedeleted: "include" | "only". - The MCP consent screen shows the server's sign-in error instead of "[object
Object]", and accounts with a second factor can consent: the screen asks for
an authenticator or recovery code. An account that only signs in with a social
provider or a magic link is told to set a password first. - Two backends built from one auth adapter instance each keep their own
internal service key. The adapter instance is no longer written to. - A function's deliberate text 404 from an
app.all(...)route is no longer
rewritten into{"error":{"code":"NOT_FOUND","message":"No route for …"}}. defineFunctionkeeps an app returned from another copy ofhono. Its
routes were dropped, and every route answered 404.backend.shutdown()stops the SQL rate-limit store's sweep timer when
the backend created the store itself.- A legacy
authschema the boot cannot drop is reported. The drop
matched only anauth.uid()that readsapp.uid, so the one every release
before 0.10.0 wrote (readingapp.user_id) kept the schema, silently. Both
bodies now match, and when the schema still stands after the drop, the boot
warns and names the objects keeping it.
CLI
-
A cloud deploy's result lists the project's own host. The control plane
reports only hostnames an app declares in itspath, so thehostsfield of
rebase cloud deploy --jsonwas[]for a project served on
<slug>.rebase.website. The project's host now comes first, as verified. -
A failed cloud deploy says which app failed and why, in one line. The
reason is read from the control plane's❌line at the end of the build
log, withrebase cloud logsnamed for the rest. The JSON error gains
apps,reasonandhint. -
The stale-build warning shows only to someone developing the CLI. A
developer's globalrebaseis often linked to a checkout of this repo, so a
barerebase cloud deployin their own project warned "dist/ is 159h older
than src/" on every run, about source they were not working on. It now warns
only when the CLI runs from a pnpm workspace checkout, not from
node_modules, and the command runs inside that checkout. Set
REBASE_CLI_DEV=1to get it when testing a dev build against another
project. -
A
rebaselinked to a checkout runs the CLI your project pinned. On a
machine whose globalrebaselinks to a checkout of this repo, a bare
rebase cloud deployin a project ran the checkout's build (280h older than
its source, where it was found) instead of the project's installed
@rebasepro/cli0.25.1; onlypnpm rebaseused the pinned one. A checkout
now hands off to the@rebasepro/clithe project installed in
node_modules, with the same arguments, and exits with its code, as jest and
gulp do.--debugnames both. A project that links its CLI to a checkout
keeps running that checkout, andREBASE_CLI_DEV=1runs the checkout
anyway, for testing a dev build against another project. -
rebase cloud compute set --scale-to-zerois retired. The dial never did
anything: every backend ran as an always-on instance whatever it said.true
is now refused with a message naming--replicasand--autoscale-max;
falseis ignored with a warning, so scripts that pass it keep working. The
help pages andrebase cloud computeno longer show it, and the CPU and
memory defaults they print are the platform's:100mand384Mi, bursting
to 2 CPU and2Gi. -
The cloud debug boot view shows the functions and crons a 0.25 runtime
loaded. It kept a line only if it said "Loaded function" or "Mounted", the
words runtimes used up to 0.24; 0.25 says "Loaded custom functions", "Loaded
cron job: …" and "Cron jobs scheduled", sorebase cloud debug bootshowed a
project's storage and its port and none of the code it runs. Both wordings are
shown now. -
Every command reads the collections where
rebase.jsonputs the backend's
config, not onlyconfig/collections: verify, doctor, generate-sdk,
console,db pushanddb generate,schema generate, dev's regeneration
and the Claude Code hook. -
A database push leaves alone the DEFAULT of a column declared
databaseDefault: true.rebase db pushplannedDROP DEFAULT, and
creates that left the field to the database then failed on NOT NULL. A DEFAULT
on an undeclared column is still removed. -
The live RLS audit no longer leaves the managed dev database read-only.
Afterrebase verify --liveornpx @rebasepro/rls-check "$(rebase db url)",
every write failed with 25006 untilrebase db stop. -
Project verification fails when it has nothing to check.
rebase verify
refuses a--collectionspath that does not exist (it reported 0 errors), and
rebase verify --livefails when the database is missing or does not answer
(it skipped and exited 0). The live audit reads the database thedbcommands
resolve, the managed one included, takes--database-urland--docker, and
counts a baselined database's earlier migrations as applied. A fresh blog
scaffold passes it: a compiledaccess: "public"read is a warning while the
API requires a token, and an error withAUTH_REQUIRE=false, read as every
boolean env var is read. -
The dev server no longer prints "Schema and SDK types regenerated" after a
failed regeneration. -
The Claude Code collection guardrail runs on Windows, and stops blocking
edits it cannot check: in a project whose dependencies are not installed
yet, or whose CLI predatesrebase verify, it skips with a note. -
Rewriting
rebase.jsonkeeps the app fields this CLI does not model, so
generate-sdk --register,apps init --forceandejectno longer delete
them. -
A mistyped flag under --json puts the error envelope on stdout, as the CLI
promises for every exit. -
A view label with braces, angle brackets, an ampersand or a comment end
still compiles.rebase generate view --namenow quotes the label. -
The skills installer updates a hook an earlier CLI wrote, instead of
reporting it as edited by you (rebase skills --hooks). -
Commands in a directory
init --claimprovisioned keep its token and claim
link.rebase cloud whoamiandprojects createdeleted both;whoamiand
statusnow show the project, its claim URL and the expiry,unlinkwarns
that an unclaimed project keeps running, a secondinit --claimpoints at the
existing claim, and every command that knows says when the project's
REBASE_API_TOKENexpires and how to replace it.init --claimdeploys with
the new project's own token even whenREBASE_TOKENis set, and commands warn
whenREBASE_TOKENdisplaces it. After a claim, a signed-in claimer is told
to run the command again instead of to sign in. In such a directory, a
REBASE_TOKENthat holds no scope on the project gives way to the project's
own token, and the first command says so; it still wins when it reaches the
project, when its scopes cannot be read, and for a command given--project
for another project. Minting the app's key retries on a 502 or 504 from the
app's gateway, and a session-only command refused there no longer says the
project's token reads the database: it deploys, reads logs and metrics, sets
variables and runsdb info, and reads no secret back. -
A bundle deploy blocked by another deploy names it and how to cancel it,
and a402points atrebase cloud billing setup. -
Concurrent
rebase cloudcommands, such as parallel MCP tools, no longer
wipe~/.rebase/credentials.json. -
rebase cloud rollbackwith no id no longer re-deploys the live
deployment. After a static-only deploy it refuses, as the docs say. -
A generated screen can no longer take a slug the admin's own routes answer
first.rebase generate viewrefusesc,sandsettingsfor a screen. -
Project verification reads a belongsTo owner column from its
relation name. Inrebase verify, aworkspacerelation with
relationName: "tenant"(columntenant_id) passed as public. -
A CI build of a fresh clone compiles typed callbacks.
rebase buildand
rebase cloud deploynow write the server-sideDatabaseregistration
(config/generated/register.tsanddatabase.types.ts, as
rebase generate-sdkwrites them) before the type check, when the backend
setsregisterDatabaseor an earlier--registerleft one. They keep that
one current and refuse to overwrite aregister.tsthey did not write. -
rebase cloud compute set --helpgives the real database defaults
(160m / 1Gi, not 500m / 2Gi), and says--storagesets the storage mode
(shared-prefixordedicated-bucket), not a volume size.
Client SDK
- A public page no longer prints the client's connection messages to every
visitor's console. In a browser, "Connected to PostgreSQL backend" and
"WebSocket auto-authenticated" were written withconsole.debugon every
load, naming the backend on any site that used the client. Connection
messages are now off by default everywhere. Turn them on with
createRebaseClient({ debug: true }), or in one browser, with no change to
the site, withlocalStorage.setItem("rebase_debug", "1");
globalThis.__REBASE_DEBUG__and__REBASE_DEBUG_REALTIME__work too.
Warnings and errors are printed as before. - A required belongsTo is satisfied by either spelling on a typed create.
Withauthordeclaredvalidation: { required: true }, the generated
Insertrequired bothauthorIdandauthor, so only a body naming the row
twice compiled, onclient.data,batch()andcontext.dataalike. Either
spelling now compiles; neither (ornull) is still an error. Regenerate with
rebase generate-sdk. - A closed to-one relation's foreign key leaves the generated types, as it
leaves the API. AnexcludeFromApibelongsTono longer puts its key on
Row,InsertorUpdate, awrite: []one no longer offers its key on
writes, andIncludeForno longer offers anexcludeFromApirelation the
server answersUNKNOWN_RELATION. - A hasOne relation is writable on a typed create and update.
client.data.users.update(1, { profile: 9 })(andnullto unlink) compiles,
as the server already performed it. - A collection callback's client takes SQL bind parameters.
context.client.sql(q, { params })was a type error whilerebase.sql
accepted it. - An offline edit or delete made after a queued create lost its answer is no
longer lost, withreplayBatchSizetoo. A caller's ownifMatchon
update()anddelete()survives the queue, with or withoutonConflict;
without it, a 412 is reported toonSyncError.findById()on an
offline-enabled client keeps the row's version (etagOf). - A socket refused for too many connections backs off and says so. Over
REALTIME_MAX_SOCKETS_PER_USER, the client redialled every second unseen; it
now backs off up to 30 s and reportsCONNECTION_LOST
(details.reason: "TOO_MANY_SOCKETS") to its subscriptions and
onConnectionLost. - With a recount interval set, a list's total is counted again when the
interval is up, not left stale until the next write
(REALTIME_RECOUNT_INTERVAL_MS, andlistenCount()). - With
offline.keepLastUser, an unconfirmed session no longer sends writes
anonymously. Once the network was back but before the session refresh
answered, writes went out with noAuthorization, an anonymous empty list
replaced the user's cached list, and an anonymous 404 deleted their cached
row. Writes now queue, reads come from the local database, and anonymous
answers are not kept. - A retained channel no longer replays a sender's own broadcast to it after
a reconnect, including from a new channel object after a reload.
Admin (CMS & app)
-
The
"Shell.AppBar","HomePage.CollectionCard"and"Auth.LoginView"
component overrides work. All three were declared, documented and shown in
the<Rebase components>example, and nothing resolved them, so setting one
did nothing. A"Shell.AppBar"override now renders a top bar (a wrapper is
handed the default one); anappBarelement on<RebaseShell>still wins,
as doesloginViewon<RebaseAuth>over"Auth.LoginView". A test now
fails when a declared override name is resolved nowhere. -
exportable: falseremoves Export from the collection toolbar. It was
ignored; only the object form was read, for its extra fields. -
The record form follows field access rules. A field the user may not
write (access.write) is shown read-only, and one they may not read
(access.read,excludeFromApi) is left out, label included. Before, both
were ordinary inputs and the edit came back as a 400 on save. A new record or
a copy no longer sends a default or copied value for a field the user may
not write, which made the server refuse the whole save. -
A language added through
<Rebase translations>is in the language
menu. The menu listed the seven shipped languages only, so an app's own
language could be set withlocaleand never chosen by the user. A
language Rebase does not ship is named by the browser (nl→ "Nederlands").
useRebaseLocales()returns the list. -
A record created from its parent's tab opens linked to that parent. A new
transaction from a customer's Transactions tab asked for the customer, and a
requiredbelongsTocould not be saved until the user picked the record they
had come from. The server already linked a create under
customers/{id}/transactionsto that customer. The form now opens with the
link set and locked while the record is new. Editing it later still works.
This covers ahasManywhose child has thebelongsToback on
foreignKeyOnTarget. A link keyed onsourceKeyis not prefilled. -
"Create" in the side panel opens the saved record instead of asking about
unsaved changes. The save reported the form clean and moved the panel to
the new record in the same tick, and the panel's guard still saw it as dirty.
It asked "There are unsaved changes" about a record already saved and left
the URL on#new_side. A dirty record still asks before the panel leaves it.
useUnsavedChangesDialognow also takes a function forwhen, read at the
moment of navigation. -
EntityFormPanelsaves the record it shows after itsentityIdchanges.
It saved into the previous record, or answered 412 on every save; changing
entityIdnow remounts the form.EntityFormPanelandCollectionPanelalso
open a subcollection by its full path (posts/p1/comments) inside a scope
that holds its parent. -
A record panel whose record is deleted elsewhere keeps what was typed.
With a live read,EntityFormPanelthrew "Form has not been initialised
with the correct parameters" mid-edit. It now does what the edit view does:
it keeps the last record and the typed values, says the record was deleted
elsewhere, and offers "Save as new", which turns the panel into a create
form with those values;onSavedreports the new record. -
A dialog never runs wider than a phone's screen. The kit's
Dialog
ignoredfullWidthwhenevermaxWidthwas set, so every default dialog was
a fixed 512px.fullWidth(the default) now gives 11/12 of the viewport
capped bymaxWidth, andfullWidth={false}givesmaxWidthcapped at
11/12. Desktop sizes are unchanged. The AI collection generator's panel fits
a phone too.
MCP
rebase_cloud_provisionsays what to do when the control plane refuses
(provisioning not enabled, rate limited, already linked), takes names up to
200 characters, and is no longer refused when the active project already
points at a remote app.
Security
- A role holding users:write can no longer grant itself, or anyone, admin. A
role name like"support,admin", a nested array or a quoted name was written
into the roles array literal as more than one role. Roles are now stored as
bound parameters, and such names are refused (see Breaking). - A route group mounted on its own refuses impersonation and simulation it
cannot honour.backend.routes.storage(),routes.admin()and the others
answered a request carryingx-rebase-impersonateorx-rebase-simulate-*as
the administrator.refuseUnhonouredImpersonationand
reportSimulatedContextare exported for your own groups. - A role with keys:write can no longer change or revoke a service key that
outranks it. A key holding a role or scope the caller does not answers
403 KEY_OUTRANKS_CALLER. - The remote MCP consent screen shows the host the authorization code is sent
to, and says that host is not verified. A client registered as "Claude" with
any https redirect looked identical to the real one. - AI Autofill sends only the values of declared, unlocked properties.
Read-only or hidden properties that also haddynamicProps, and columns the
collection does not declare, were sent to the AI service
(@rebasepro/plugin-ai). - The MCP server sends the project's local service key only to a loopback
backend. Afterrebase cloud init --claimwithout a deploy,
@rebasepro/mcpsent it to the cloud project; it now says on stderr to set
REBASE_API_TOKEN. - The MCP server no longer sends the CLI's control-plane token to the app.
REBASE_TOKENwas accepted as the app's API key, so the token that can deploy
and read secrets reached every backend the MCP server called. Set
REBASE_API_TOKENfor the app; the server says so once on stderr when only
REBASE_TOKENis set. Setups that relied on the alias stop authenticating
with the app. - Every CSV the product writes escapes spreadsheet formulas, as the admin's
export already did: the SQL console's Export CSV, Copy as CSV and Download
full result as CSV prefix'to a cell starting with=,+,-,@, a
tab or a carriage return, while signed numbers stay numbers. Such cells are no
longer byte-identical toCOPY … CSV. - A cloud claim's .env is private and keeps your token.
rebase cloud init --claimcreates.envwith mode 0600 and no longer
overwrites aREBASE_API_TOKENalready in it. - A remote MCP access token stops working as soon as its account is
deleted, disabled or signed out everywhere, and runs with the account's
current roles./mcpchecked only the token's signature, so for up to an
hour it kept acting for a deleted or disabled account, after a password
change, and with roles a demotion had removed. Every request now re-reads
the account, as the other doors do; a failed lookup answers 503. - The
_rebase/maintenance/storage prefix, where orphan-scan records live,
is reserved. Reading, writing or listing it answers400 INVALID_STORAGE_KEY, so a caller cannot forge what an administrator's
orphan delete removes. - An OAuth sign-in whose provider verified the email verifies the matching
unverified account, after removing what nobody proved on it. Before linking,
the password, every identity whose provider did not vouch for the address
and every session are removed, as a magic link or email code already does, so
someone who pre-registered the victim's address keeps no way in. The refusal
reasonslocal-account-unverifiedandlocal-account-unverified-passwordless
are no longer returned. A provider that does not assertemail_verified
still gets403 EMAIL_NOT_VERIFIED(provider-email-unverified), and a user
store withoutunlinkUserIdentityanswers409 UNVERIFIED_IDENTITIESand
changes nothing. A user who never verified their password loses it on their
first provider sign-in.
Documentation
- Embedding the Postgres Driver and The Remote MCP Endpoint are pages of their
own.