ElementsGate

RLS

Row-level security — Gate identity stamped into SQL so store.schema.policy helpers filter every fx.store query.

Gate decides whether a caller may run a Flow. RLS decides which SQL rows that caller may see or change.

After .gate(...) passes, fx.store stamps the principal into Postgres session GUCs (oke.gate · oke.user · oke.scopes · oke.tenant) so table policies enforce the same identity.

For developers shipping multi-user SQL on okengine — declare Gate policies, attach them on the trigger, and mirror them on the table with store.schema.policy.*.

The one rule

Put row predicates on the table (store.schema.policy.gate / owner / scope / tenant). Put permission to act on the trigger (.gate(...)). Never invent a separate middleware layer — the stamp rides every fx.store call.

Smallest Example

Declare a Gate policy and an RLS table

src/core/gate.ts
import { gate } from "okengine";

export const member = gate.policy("member", ({ auth }) => !!auth.verified);
src/db/schema.decl.ts
import { store, field } from "okengine";

export const tasks = store.schema.table(
  "tasks",
  {
    id: field.id().primaryKey(),
    owner: field.text().notNull(),
    title: field.text().notNull(),
  },
  [
    store.schema.policy.gate("member", { for: "select" }),
    store.schema.policy.owner("owner", { for: "all" }),
  ],
);

Gate the route and query through fx.store

src/flows/tasks/list.ts
import { on, flow, http } from "okengine";
import { member } from "@/core/gate";
import { db, tasks } from "@/schema";

export const list = on(
  http.get().gate(member),
  flow({
    do: async (_, fx) => fx.store(db).select().from(tasks),
  }),
);

What the caller sees

Alice’s session stamps oke.gate = 'member' and oke.user = '<alice>'. SELECT policies that require oke.gate() = 'member' and owner = oke.user() return only Alice’s rows — even if the Flow’s where clause is empty.

Use an RLS-capable SQL driver (postgres / pglite). The memory SQL driver does not enforce row policies.

Progressive Patterns

From a gate-name check to owner columns, scopes, and tenants:

store.schema.policy.gate(name) stamps oke.gate() = '…' (default for: "select"). The name must match a Gate policy on the trigger (rate gates are skipped when picking the stamp):

store.schema.policy.gate("member", { for: "select" });

Consequence: the first non-rate gate on .gate(...) becomes oke.gate for that request. Put the policy you want stamped first when the chain mixes policies and rates.

Identity Stamp

After Gate allows, the SQL session prelude sets:

GUC / helperSourceUsed by
oke.gate()First non-rate gate on the triggerpolicy.gate
oke.user()fx.auth.userIdpolicy.owner
oke.has_scope(…)fx.auth.scopes (JSON)policy.scope
oke.tenant()fx.tenant.id when tenancy is onpolicy.tenant

The stamp SET LOCAL ROLE oke_app and turns row_security on so table owners cannot bypass policies on the hot path.

Plane / modeStamp?
User-plane Flow, gatedYes
plane: "operator"No — operator bypasses RLS
Console / Call API bypassNo

Policy Helpers

Extras are the third argument of store.schema.table(name, cols, extras):

HelperDefault forPredicate
store.schema.policy.gate(name)selectoke.gate() = '…'
store.schema.policy.owner(col)allcol = oke.user()
store.schema.policy.scope(scope)insertoke.has_scope('…')
store.schema.policy.tenant(col)allcol = oke.tenant()
store.schema.rls()Enable RLS with no policies yet
store.schema.unscoped()Opt out of tenant requirement
store.schema.policy(name, opts)Raw using / withCheck / as / to

for accepts select · insert · update · delete · all. Optional as: "permissive" (default) or "restrictive". Optional to limits Postgres roles.

src/db/schema.decl.ts
import { store, field } from "okengine";
import { member, bookingCreate } from "@/core/gate";

export const bookings = store.schema.table(
  "bookings",
  {
    id: field.id().primaryKey(),
    owner: field.text().notNull(),
    tenantId: field.text().notNull(),
  },
  [
    store.schema.policy.gate("member", { for: "select" }),
    store.schema.policy.owner("owner", { for: "all" }),
    store.schema.policy.scope(bookingCreate, { for: "insert" }),
    store.schema.policy.tenant("tenantId"),
  ],
);

Detailed section

Schema extras, relations, and emit live under Store · SQL · Schema Extras & RLS. This page is the Gate ↔ row identity contract.

Composition

Multiple PERMISSIVE policies for the same command OR together — a row is visible if any permissive policy passes. RESTRICTIVE policies AND with the rest (as: "restrictive").

[
  store.schema.policy.gate("member", { for: "select" }),
  store.schema.policy.owner("owner", { for: "select" }),
];

A member principal sees rows that pass the gate predicate or the owner predicate (for SELECT). Tighten with restrictive policies or a single compound raw using.

Live Queries & Resources

Resource live (store.resource({ live: true })) and handwritten liveQuery classify CDC events through the same RLS stamp. Needs postgres / pglite and a gated identity.

See HTTP · Resource Live and Store · SQL.

Troubleshooting

Learn more

  • Authorizationgate.policy / gate.scope on triggers
  • Tenancyfx.tenant.id and policy.tenant
  • Store · SQL — schema extras, session handle, drivers
  • HTTP — resource live + RLS

Next

On this page