Upgrading
Questi contenuti non sono ancora disponibili nella tua lingua.
Upgrading an existing app
Section titled “Upgrading an existing app”This release renames packages, changes what id means, drops CJS, and — most
importantly — fixes a security rule that was silently permissive. Read the
first section before anything else: it is the only change that can alter who can
read your data, and it is the only one that will not announce itself.
Work through the sections in order. Each one names the symptom you would otherwise debug from the wrong end.
1. policy.authenticated() — read this first
Section titled “1. policy.authenticated() — read this first”What changed
Section titled “What changed”policy.authenticated() used to compile to:
auth.uid() IS NOT NULLOn the user path that is a tautology. applyAuthContext coerces a blank
user id to the 'anonymous' sentinel — deliberately, so that it can never read
back as NULL and be mistaken for the trusted server context. So IS NOT NULL
was true for anonymous visitors too.
A rule that reads as “logged-in users only” therefore granted access to everyone, including signed-out visitors. It now compiles to:
auth.uid() IS NOT NULL AND auth.uid() <> 'anonymous'policy.not(policy.authenticated()) was separately special-cased to mean “the
server context”. It no longer is — use policy.serverContext() for that.
Why this is the dangerous one
Section titled “Why this is the dangerous one”The compiled SQL lives in your database, not in your application code. Upgrading the packages does not change it. The two failure modes are opposite, and both are quiet:
| What you do | What happens |
|---|---|
Upgrade packages, do not re-run db push |
Your database keeps auth.uid() IS NOT NULL. Anonymous visitors keep the access they should never have had. Nothing warns you. |
Upgrade packages and re-run db push |
The policy tightens. Anything that was relying on the permissive behaviour — an unauthenticated read your frontend does on page load, a public listing, a webhook without a session — starts returning zero rows or 403. |
rebase doctor --policiescatches this, from 0.10.0 on. It reads the livequalandwith_checkstraight out ofpg_policiesand reports, under Insecure, any policy still carrying the bareauth.uid() IS NOT NULLtautology. It reports the other half of the problem under Orphaned: a policy an earlier push superseded but never dropped. Editing a rule renames its policy — the generated name is a hash of the rule — so the old one is left behind, and Postgres ORs permissive policies together, which makes an abandoned grant outrank the tightening that replaced it. The command exits non-zero, so CI can gate on it.Plain
rebase doctorruns the same policy checks alongside the schema diff;--policiesis the policies-only form, and the one to point at a deployed database. Both needDATABASE_URL(orADMIN_CONNECTION_STRING) — without it the policy checks are skipped with a warning, not failed.
What the scan does not catch. It matches that one expression shape — in whatever whitespace Postgres stored it in — and treats an
<> 'anonymous'(or!= 'anonymous') guard anywhere in the same clause as the corrected form. A fail-open policy written by hand in some other spelling —USING (true),USING (1 = 1),USING (current_setting('rebase.uid', true) IS NOT NULL)— is not flagged, and neither is a compound expression that happens to mention'anonymous'in an unrelated branch. It also needs collections to compare against: a project whose collections generate no policies at all gets no scan. Thepg_policiesread in Step 3 is how you see the expressions yourself.
Nothing applies the fix for you. Policies are not re-run at container boot. Upgrading the packages, redeploying and restarting all leave
pg_policiesexactly as it was. Onlydb pushrewrites it — and it is what drops the superseded policies too.
What to do
Section titled “What to do”Step 1 — find every affected rule. From your project root:
grep -rn "authenticated()" config/collections/Every hit is a rule whose meaning changed. Also check for the raw spelling, which was the other way to write the same tautology:
grep -rn "auth.uid() IS NOT NULL" config/collections/Step 2 — decide what each one meant. For each rule, ask which you intended:
- “Any signed-in user” →
policy.authenticated(). No code change; the behaviour is now what you wrote. Re-rundb push. - “Anyone at all, including anonymous” → you were relying on the bug, whether
you knew it or not. Make it explicit:
{ operation: "select", access: "public" }. - “Only the trusted server context” → replace
policy.not(policy.authenticated())withpolicy.serverContext().
Step 3 — check what your database actually has, before and after:
SELECT tablename, policyname, cmd, qualFROM pg_policiesWHERE schemaname = 'public'ORDER BY tablename, policyname;Any qual containing auth.uid() IS NOT NULL without the
<> 'anonymous' clause is a stale permissive policy. rebase doctor --policies
reports exactly those, and the superseded policies alongside them; this query is
how you read the expressions yourself, which is what catches a fail-open policy
written in a spelling the detector does not match.
Step 4 — re-run db push and re-run the query. db push applies the current
policies and then drops the ones an earlier push superseded. Confirm every policy
you expected to change did change, then run:
rebase doctor --policiesIt should exit 0 with no Insecure or Orphaned entries.
Step 5 — test signed-out. Open your app in a private window with no session and exercise the read paths. This is where you find the public listing that quietly depended on the old behaviour.
2. The authenticated principal is uid, not userId
Section titled “2. The authenticated principal is uid, not userId”Tokens now carry a uid claim and c.get("user") returns { uid, roles }.
grep -rn "\.userId\|payload.userId\|user.userId" src/ config/Anything reading payload.userId or user.userId gets undefined — which,
in a permission check, usually fails open or fails silently rather than
throwing. Search for the defensive a ?? b spelling too; several places had
independently grown one to cope with the two names:
grep -rn "uid ?? \|?? .*userId" src/ config/3. id is an address, not a column
Section titled “3. id is an address, not a column”Rows now carry their own columns under their own names and types. Previously a
synthesized id was written into rows on the way out, which collided with your
data three ways: it renamed the key (a sku primary key was served as id,
with sku absent), it changed the type (an integer key arrived as "42"), and
it destroyed real values (drizzleResultToRow spread it last, so it won over a
genuine id column).
If your tables are keyed on id, nothing changes for you.
If any table is keyed on something else, code reading row.id must read the
real key. Note the type change too: a numeric primary key now arrives as a
number, so row.id === "42" becomes row.sku === 42. Strict equality against
a string will silently stop matching.
4. ESM only
Section titled “4. ESM only”main, module and the import condition all point at index.es.js; the
require condition is gone. The CJS/UMD half was never loadable anyway — the
output banner injects import / import.meta.url, which a UMD bundle cannot
parse as CommonJS — so this removes a build target that could not have been
working for you.
A CommonJS consumer must use dynamic import() or move to ESM.
5. Package renames
Section titled “5. Package renames”Import paths only — no behaviour moved with them.
| Old | New |
|---|---|
@rebasepro/core |
@rebasepro/app |
@rebasepro/server-core |
@rebasepro/server |
@rebasepro/server-postgresql |
@rebasepro/server-postgres |
@rebasepro/server-mongodb |
@rebasepro/server-mongo |
@rebasepro/client-postgresql |
@rebasepro/client-postgres |
@rebasepro/client-firebase |
@rebasepro/firebase |
@rebasepro/formex |
@rebasepro/forms |
@rebasepro/sdk-generator |
@rebasepro/codegen |
@rebasepro/schema-inference |
@rebasepro/inference |
@rebasepro/mcp-server |
@rebasepro/mcp |
@rebasepro/plugin-data-enhancement |
@rebasepro/plugin-ai |
Unchanged: types, utils, common, client, admin, admin, studio,
cli, plugin-insights.
The retired names are deprecated on npm, so installing one tells you rather than resolving to an abandoned version.
Additionally, @rebasepro/auth is removed. useRebaseAuthController,
fetchAuthConfig, createAuthConfigCache and clearAuthConfigCache now come
from @rebasepro/app, beside the RebaseAuth and LoginView components they
are used with.
RebaseCMS is now RebaseAdmin. mode: "cms" on RebaseBackendConfig is
unchanged — it describes where collections come from, not the UI.
6. defaultSecurityRules moved off the server config
Section titled “6. defaultSecurityRules moved off the server config”It used to live on RebaseBackendConfig, where it enforced nothing: db push
generates the Postgres policies — the only thing that actually enforces
access — from the collection files, and never sees the running server.
Declare it in config/collections/index.ts instead, where the loader reads it
and both the runtime and db push see the same thing:
// config/collections/index.tsexport const defaultSecurityRules: SecurityRule[] = [ { operation: "select", access: "public" }, { operations: ["insert", "update", "delete"], roles: ["admin"] }];The old documentation claimed collections without rules were “unrestricted”. They are not — the generator locks them to admin-only.
In baas mode there are no collection files and no db push, so the
database’s own RLS is the whole model and there is nothing to default.
7. Smaller behaviour changes
Section titled “7. Smaller behaviour changes”A write naming a field the collection lacks is now a 400. Unknown keys used
to travel into the INSERT, so a typo came back as column "titel" does not exist — phrased by Postgres, from a stack the caller cannot see, and only when
the column really was absent. Bulk writes are checked before the transaction
opens and report the offending row index.
Auth collections are checked too, with one narrow exemption. A signup body carries credential fields —
passwordabove all — that the users collection does not declare as columns, so the auth adapter names those explicitly and everything else is validated as usual. A typo likeemialon a signup is a 400, same as on any other collection. (An auth collection wired to a customonCreateUserhook opts out of the check, because the hook, not the collection, then defines the body’s shape.)
A collection file that fails to import is now a hard error. The loader used to log and continue, turning a broken file into a missing API route and a missing policy with a successful exit code. Both read as “no data” rather than as a failure.
BaaS mode does not serve tables without row-level security. A table with RLS
disabled is skipped and named at boot. baas: { unprotectedTables: "serve" }
restores the old behaviour.
Upgrade checklist
Section titled “Upgrade checklist”[ ] grep for authenticated() and auth.uid() IS NOT NULL in config/collections/[ ] decide the intent of each rule; rewrite the ones that meant "public"[ ] replace not(authenticated()) with serverContext()[ ] SELECT ... FROM pg_policies — record the qual of every policy BEFORE[ ] update package names and imports[ ] move defaultSecurityRules into config/collections/index.ts[ ] grep for .userId[ ] grep for row.id on tables not keyed on id[ ] run db push[ ] run rebase doctor --policies — expect no Insecure or Orphaned entries[ ] SELECT ... FROM pg_policies again — confirm every intended change landed[ ] exercise the app signed OUT, in a private windowrebase doctor --policies is the one to wire into CI: it exits non-zero on a
policy still carrying the permissive tautology, and on one an earlier push
superseded. The pg_policies reads before and after are still worth the minute —
they show you the expressions themselves, which is the only way to spot a
fail-open policy written in a spelling the detector does not match. Neither the
packages nor the type system will tell you: the change lives in the database, and
db push is the only thing that puts it there.
