/**
* Row-level security rules for a collection.
*
* 325 lines of RLS policy contract that used to sit in the middle of
* `collections.ts`, between the admin panel's table view-models and the auth
* collection config. It has no relationship to the shape of a collection — it is
* the most-read part of the BaaS surface, and it now reads on its own.
*
* A rule is compiled to Postgres `CREATE POLICY` DDL and, for the structured
* flavour, independently evaluated in JavaScript, so the admin UI's idea of what
* a user may do derives from the same expression the database enforces.
*
* Note that rule *names* are not what you write: an unnamed rule compiles to
* `
__`, plus an injected `default_admin` baseline. Derive
* them with `getPolicyNamesForRule`/`getEffectiveSecurityRules` rather than
* matching on `rule.name`.
*/
import type { PolicyExpression } from "./policy";
/**
* SQL operation that a policy applies to.
* @group Models
*/
export type SecurityOperation = "select" | "insert" | "update" | "delete" | "all";
/**
* Flexible Row Level Security rule for a collection.
*
* Built on PostgreSQL Row Level Security. Rules can range from
* simple convenience shortcuts to fully custom SQL expressions, giving you the
* full power of PostgreSQL Row Level Security.
*
* The authenticated user's identity is available in raw SQL via:
* - `rebase.uid()` — the user's ID
* - `rebase.roles()` — comma-separated app role IDs
* - `rebase.jwt()` — full JWT claims as JSONB
*
* These are set automatically per-transaction by the backend.
*
* **How rules combine:** PostgreSQL evaluates all matching policies for an
* operation. Permissive rules are OR'd together (any one passing is enough).
* Restrictive rules are AND'd (all must pass). This is standard PostgreSQL RLS behavior.
*
* **Mutual exclusivity:** `ownerField`, `access`, structured `condition`, and
* raw SQL (`using`/`withCheck`) cannot be combined. The type system enforces
* this — attempting to set conflicting fields will produce a compile-time
* error.
*
* **Which form to reach for:** prefer the structured {@link StructuredSecurityRule}
* (`condition`/`check`) or the shortcuts (`ownerField`, `access`, `roles`). These
* are engine-agnostic and evaluated identically by the database and the admin UI,
* so the UI never shows an action the database will reject. Raw SQL
* ({@link RawSQLSecurityRule}) keeps full PostgreSQL power but is Postgres-only
* and server-authoritative (the UI cannot evaluate arbitrary SQL locally).
*
* @group Models
*/
export type SecurityRule =
| OwnerSecurityRule
| PublicSecurityRule
| StructuredSecurityRule
| RawSQLSecurityRule
| RolesOnlySecurityRule;
/**
* Shared fields for all SecurityRule variants.
* @group Models
*/
export interface SecurityRuleBase {
/**
* Optional human-readable name for the policy.
* If not provided, one will be auto-generated from the table name and operation.
* Must be unique per table.
*
* When using `operations` (array), each generated policy will have the
* operation name appended, e.g. `"owner_access_select"`, `"owner_access_update"`.
*/
name?: string;
/**
* Which SQL operation this policy applies to.
* Use this when the policy targets a single operation or all operations.
*
* For multiple specific operations, use `operations` (array) instead.
* If neither is specified, defaults to `"all"`.
*
* @default "all"
*/
operation?: SecurityOperation;
/**
* Array of SQL operations this policy applies to.
* The compiler will generate one PostgreSQL policy per operation, sharing
* the same configuration.
*
* This reduces boilerplate when the same rule applies to multiple (but not all)
* operations.
*
* Takes precedence over `operation` (singular) if both are specified.
*
* @example
* // Same rule for select and update
* { operations: ["select", "update"], ownerField: "user_id" }
*
* @example
* // Equivalent to operation: "all"
* { operations: ["all"], ownerField: "user_id" }
*/
operations?: readonly SecurityOperation[];
/**
* Whether this policy is `"permissive"` (default) or `"restrictive"`.
*
* - **permissive**: Multiple permissive policies for the same operation are
* OR'd together — if *any* passes, access is granted.
* - **restrictive**: Restrictive policies are AND'd with all permissive
* policies — they act as additional gates that *must* also pass.
*
* This is the standard PostgreSQL RLS model.
*
* @default "permissive"
*/
mode?: "permissive" | "restrictive";
/**
* **Shortcut.** Restrict this rule to users that have one of these
* application-level roles.
*
* **Important:** These are NOT native PostgreSQL database roles — names
* like `public`, `anon` or `authenticated` belong to {@link pgRoles} and
* produce a condition no user can satisfy if used here. These are
* application roles managed by Rebase, stored as an inline `roles TEXT[]`
* column on the users table, and injected into each transaction as
* `app.user_roles` — which `rebase.roles()` reads.
*
* There is no roles registry: a role exists once it is assigned to a user.
*
* Generates a safe array-overlap condition — the user passes if they hold
* *any* of the listed roles:
* `string_to_array(rebase.roles(), ',') && ARRAY['', '']`
*
* (Note: this is a true set intersection, NOT a regex/substring match, so
* a role named `admin` never matches `nonadmin` or `superadmin`.)
*
* Can be combined with `ownerField`, `access`, `condition`, or raw
* `using`/`withCheck`. When combined, the role check is AND'd with the
* other condition.
*
* @example
* // Only admins can delete
* { operation: "delete", roles: ["admin"] }
*
* @example
* // Admins have unfiltered read access to all rows
* { operation: "select", roles: ["admin"], using: "true" }
*/
roles?: readonly string[];
// ── Advanced: native PostgreSQL role targeting ───────────────────────
/**
* **Advanced.** Native PostgreSQL database roles the policy applies to.
*
* By default, all generated policies target the `public` role (i.e.
* every database connection). This is correct for most setups where
* a single database role is used for all connections.
*
* **Important:** These are NOT the same as the application-level
* {@link roles} (admin, editor, viewer, etc.) — those are enforced in the
* USING/WITH CHECK clauses via `rebase.roles()`. This field controls the
* PostgreSQL `TO` clause in `CREATE POLICY ... TO role_name`.
*
* Use this if you have dedicated PostgreSQL roles (e.g. `app_read`,
* `app_write`) and want policies to target specific ones.
*
* @default ["public"]
*
* @example
* // Only apply this policy when connected as `app_role`
* { operation: "select", access: "public", pgRoles: ["app_role"] }
*/
pgRoles?: readonly string[];
}
/**
* Security rule that grants access based on row ownership.
* Generates a USING/WITH CHECK clause like: ` = rebase.uid()`
*
* Cannot be combined with `using`, `withCheck`, or `access`.
*
* @example
* { operation: "all", ownerField: "user_id" }
*
* @group Models
*/
export interface OwnerSecurityRule extends SecurityRuleBase {
/** The property (column) that stores the owner's user ID. */
ownerField: string;
access?: never;
using?: never;
withCheck?: never;
condition?: never;
check?: never;
}
/**
* Security rule that grants unrestricted row access (no row filtering).
* Generates `USING (true)`.
*
* This means "no row-level filter", NOT "anonymous/unauthenticated access".
* Authentication is still enforced at the API layer — this only controls which
* *rows* authenticated users can see.
*
* Cannot be combined with `using`, `withCheck`, or `ownerField`.
*
* @example
* // Public read (any authenticated user sees all rows)
* { operation: "select", access: "public" }
*
* @group Models
*/
export interface PublicSecurityRule extends SecurityRuleBase {
/** Grant unrestricted row access for this operation. */
access: "public";
ownerField?: never;
using?: never;
withCheck?: never;
condition?: never;
check?: never;
}
/**
* Security rule expressed as a structured, engine-agnostic
* {@link PolicyExpression}. This is the **recommended** way to write a
* non-trivial condition: it compiles to PostgreSQL `USING`/`WITH CHECK` SQL
* *and* is evaluated identically by the admin UI, so the UI can never show an
* action the database will reject.
*
* Cannot be combined with `ownerField`, `access`, or raw `using`/`withCheck`.
*
* @example
* // Owner, or any user holding the `moderator` role
* {
* operation: "update",
* condition: policy.or(
* policy.compare(policy.field("user_id"), "eq", policy.authUid()),
* policy.rolesOverlap(["moderator"])
* )
* }
*
* @group Models
*/
export interface StructuredSecurityRule extends SecurityRuleBase {
/**
* Structured condition for the `USING` clause — which *existing* rows are
* visible / can be modified / deleted (SELECT, UPDATE, DELETE).
*/
condition: PolicyExpression;
/**
* Structured condition for the `WITH CHECK` clause — which *new/updated*
* row values are allowed (INSERT, UPDATE). Defaults to `condition` when
* omitted, mirroring PostgreSQL's own behavior.
*/
check?: PolicyExpression;
ownerField?: never;
access?: never;
using?: never;
withCheck?: never;
}
/**
* Security rule using raw SQL expressions for full PostgreSQL RLS power.
*
* **Postgres-only and server-authoritative.** Arbitrary SQL cannot be
* evaluated by the admin UI, so a rule using this form is treated as *unknown*
* client-side (never silently allowed) and its effect on visible actions is
* reflected from the server. For conditions that should also drive the UI
* precisely, prefer the structured {@link StructuredSecurityRule}.
*
* Cannot be combined with `ownerField`, `access`, or structured `condition`.
*
* You can reference columns via `{column_name}` which will be resolved to
* `table.column_name` in the generated Drizzle code.
*
* @example
* // Rows published in the last 30 days are visible
* { operation: "select", using: "{published_at} > now() - interval '30 days'" }
*
* @example
* // Only the owner, or users with 'moderator' role
* {
* operation: "select",
* using: "{user_id} = rebase.uid() OR rebase.roles() ~ 'moderator'"
* }
*
* @group Models
*/
export interface RawSQLSecurityRule extends SecurityRuleBase {
/**
* Raw SQL expression for the `USING` clause.
* This controls which *existing* rows are visible / can be modified / deleted.
* Applied to SELECT, UPDATE, and DELETE.
*/
using: string;
/**
* Raw SQL expression for the `WITH CHECK` clause.
* This controls which *new/updated* row values are allowed.
* Applied to INSERT and UPDATE.
*
* If not provided on INSERT/UPDATE policies, falls back to `using`
* (which matches PostgreSQL's own default behavior).
*/
withCheck?: string;
ownerField?: never;
access?: never;
condition?: never;
check?: never;
}
/**
* Security rule that only filters by application roles, without any
* row-level condition (USING/WITH CHECK).
*
* Useful for simple "only admins can access this table" rules where
* no per-row filtering is needed.
*
* @example
* // Only admins can delete
* { operation: "delete", roles: ["admin"] }
*
* @group Models
*/
export interface RolesOnlySecurityRule extends SecurityRuleBase {
ownerField?: never;
access?: never;
using?: never;
withCheck?: never;
condition?: never;
check?: never;
}