Skip to content

Commit 9e3e141

Browse files
committed
docs(spec): RowLevelSecurityPolicySchema.check states the per-operation using default
The using-to-check default is decided per write operation across the applicable policies, not policy by policy: when any applicable policy declares check, only the declared checks decide and a USING-only sibling adds nothing. The describe and TSDoc said the default applied per policy. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3b56070 commit 9e3e141

1 file changed

Lines changed: 21 additions & 5 deletions

File tree

‎packages/spec/src/security/rls.zod.ts‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,26 @@ export const RowLevelSecurityPolicySchema = lazySchema(() => strictObject(
399399
* Similar to USING but applies to new/modified rows.
400400
* Prevents users from creating/updating rows they wouldn't be able to see.
401401
*
402-
* **Default Behavior**: If not specified, implementations should use the
403-
* USING clause as the CHECK clause. This ensures data integrity by preventing
404-
* users from creating records they cannot view.
405-
*
402+
* **Default Behavior**: the `using` → `check` default is decided per write
403+
* operation across the applicable policies, not policy by policy. A policy
404+
* is applicable to a write when it is not `enabled: false`, its `object` is
405+
* the written object or `'*'`, its `operation` is the write's own (`insert`
406+
* or `update`) or `'all'`, and, when it lists `positions`, the caller holds
407+
* one of them.
408+
*
409+
* - When any applicable policy declares `check`, only the declared checks
410+
* decide, OR-combined. A USING-only sibling adds nothing: its `using` is
411+
* not part of the write check.
412+
* - When none declares `check`, each applicable policy's `using` stands in as
413+
* its check, OR-combined. The platform's own ownership floor
414+
* (`owner_only_writes`) takes part only where the by-id pre-image gate
415+
* kept it.
416+
*
417+
* So declaring `check` on one policy replaces, for the callers that policy
418+
* applies to, the `using` its USING-only siblings would otherwise have
419+
* contributed to the check. A `check` on a `select` or `delete` policy is
420+
* never evaluated and takes no part in this choice.
421+
*
406422
* Use cases:
407423
* - Prevent cross-tenant data creation
408424
* - Enforce mandatory field values
@@ -415,7 +431,7 @@ export const RowLevelSecurityPolicySchema = lazySchema(() => strictObject(
415431
*/
416432
check: z.string()
417433
.optional()
418-
.describe('Validation condition for INSERT/UPDATE (defaults to USING clause if not specified - enforced at application level)'),
434+
.describe('Validation condition for INSERT/UPDATE, matched against the new row (enforced at application level). The default to `using` is decided per operation across the applicable policies, not per policy: when any applicable policy for that operation declares `check`, only the declared checks decide (OR-combined) and a USING-only sibling adds nothing; only when none declares `check` does each applicable policy\'s `using` stand in as its check (OR-combined). Applicable = not `enabled: false`, `object` matches or is \'*\', `operation` matches or is \'all\', and the caller holds one of its `positions` when it lists any. A `check` on a `select` or `delete` policy is never evaluated.'),
419435

420436
/**
421437
* Restrict this policy to specific positions (ADR-0090 D3; formerly

0 commit comments

Comments
 (0)