Ir al contenido

Upgrading

Esta página aún no está disponible en tu idioma.

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”

policy.authenticated() used to compile to:

auth.uid() IS NOT NULL

On 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.

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 --policies catches this, from 0.10.0 on. It reads the live qual and with_check straight out of pg_policies and reports, under Insecure, any policy still carrying the bare auth.uid() IS NOT NULL tautology. 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 doctor runs the same policy checks alongside the schema diff; --policies is the policies-only form, and the one to point at a deployed database. Both need DATABASE_URL (or ADMIN_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. The pg_policies read 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_policies exactly as it was. Only db push rewrites it — and it is what drops the superseded policies too.

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-run db 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()) with policy.serverContext().

Step 3 — check what your database actually has, before and after:

SELECT tablename, policyname, cmd, qual
FROM pg_policies
WHERE 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 --policies

It 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/

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.


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.


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.ts
export 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.


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 — password above 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 like emial on a signup is a 400, same as on any other collection. (An auth collection wired to a custom onCreateUser hook 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.


[ ] 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 window

rebase 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.