Row-Level Security
KalamDB 0.6 applies FORCE row-level security to every SHARED table. You grant visible and
writable rows with PostgreSQL-shaped CREATE POLICY statements. There is no ACCESS_LEVEL table
option.
This page is the SQL reference. For compile, bind, MVCC ordering, and live keyed routing, see /docs/server/architecture/row-level-security.
Row-level security applies to SQL scans, DML, live subscriptions, and FILE downloads for user
and service sessions. system and dba bypass RLS. Anonymous sessions cannot open shared tables.
Default Deny
A shared table with no matching policy returns zero rows on SELECT and rejects writes for
user and service. Create at least one policy before those roles can see or mutate data.
USER and STREAM tables stay partitioned by the effective user_id. They do not use
CREATE POLICY. See /docs/server/architecture/table-types.
CREATE POLICY
AS RESTRICTIVE is rejected. Policies are always permissive: if any applicable policy allows the
row, the row is allowed.
TO selects which roles the policy applies to:
| Target | Who it applies to |
|---|---|
TO user | End-user sessions |
TO service | Service-account sessions |
TO user, service | Both authenticated principals |
TO PUBLIC (or omit TO) | Every role subject to RLS (user and service) |
Named principals such as TO alice are not supported. Bind identity in the expression with
CURRENT_USER (or CURRENT_USER()).
Policy DDL (CREATE / ALTER / DROP POLICY) requires system, dba, or service. Regular
user sessions cannot change policies.
USING vs WITH CHECK
| Command | USING | WITH CHECK |
|---|---|---|
SELECT | Required. Filters visible rows. | Not allowed. |
INSERT | Not allowed. | Required. Must hold for the new row. |
UPDATE | Existing row must pass. | New row must pass. |
DELETE | Existing row must pass. | Not allowed. |
ALL | Existing-row check. | New-row check for insert/update. |
Client WHERE clauses, including OR true, cannot bypass RLS. Authorized MVCC winners are
selected first, then the query filter runs.
Examples
The IN (SELECT …) form and a correlated EXISTS compile to the same membership relation:
ALTER POLICY and DROP POLICY
DROP POLICY CASCADE is not supported.
Supported USING expressions
KalamDB compiles policy SQL into bounded authorization semantics. These shapes are supported:
| Shape | Example | Live routing |
|---|---|---|
| Column equals current user | owner_id = CURRENT_USER | Keyed on that column and principal |
| Column equals a literal | visibility = 'public' | Keyed on that literal |
Membership IN subquery | conversation_id IN (SELECT … WHERE user_id = CURRENT_USER) | Keyed on each membership value |
Correlated EXISTS | EXISTS (SELECT 1 FROM members …) | Same keys as the equivalent IN |
| Allow all | true | Every change on the table is a candidate |
| Deny all | false | No live candidates |
Membership subqueries may add static predicates on the relation, for example
AND role = 'member'. The live index is still the protected key (conversation_id in the examples
above).
Give the members table a covering primary key on (principal, relation_key) so lookups can probe
the index instead of scanning the relation:
Rejected policy shapes
These fail at CREATE POLICY / ALTER POLICY because they cannot produce a bounded live route:
- Row-local
NOT,AND, orORsuch asNOT (owner_id = CURRENT_USER)orowner_id = CURRENT_USER OR is_public = true - Negated
IN/EXISTS - Aggregates in the membership subquery (
max(conversation_id)) - Membership subqueries that do not restrict
CURRENT_USER - Per-user
TO <user_id>lists
Keep extra predicates in the client WHERE clause, not in the policy, when they are not a bounded
equality or membership key.
Live subscriptions
Shared-table live queries bind RLS once at subscribe time. Each change looks up subscribers by the
policy keys (for example conversation_id) instead of evaluating every subscriber on the table.
The bound policy is still the final check. If a grant, revoke, or membership change races an in-flight event, delivery fail-closes: the row is not leaked. The client should resubscribe after a fail-closed gap.
USING (true) broadcasts every table change to those subscribers. Prefer keyed owner or membership
policies when many users share one table.
Existing live subscriptions do not pick up a newly created policy until the client resubscribes.
See /docs/server/architecture/row-level-security, /docs/server/architecture/live-query, and /docs/server/sql-reference/subscriptions.
Writes and upserts
INSERT, UPDATE, and DELETE use the same compiled policies. user and service
ON CONFLICT DO UPDATE on shared tables is rejected so upsert cannot skip USING / WITH CHECK.
Use a plain INSERT or UPDATE instead.
EXECUTE AS does not change shared-table RLS. The acting role and CURRENT_USER still come from
the session that opened the statement. See
/docs/server/sql-reference/impersonation.