database.withRls(). PostgreSQL then applies your policies inside the transaction.
RLS is an explicit choice for each application table. Existing databases and applications keep their current behavior until you add policy and enforcement migrations. Authentication tables are not automatically changed.
Use the coordinated 0.1.30 or later customer-runtime release with the matching deployed database service, CLI and Skills. Install the exact runtime alias returned by ohmyhost init --dry-run --json and commit the updated lockfile. An older service may refuse an RLS call; report that failure instead of falling back to an ordinary query.
Protect a table
Add a new immutable migration. This example gives each signed-in user access to their own notes:USING controls which existing rows the user can see or change. WITH CHECK controls the rows they can insert and the new values they can save. In this example, a user cannot read another user’s note or change a note’s owner to someone else. Anonymous access has no user ID and matches no rows.
Every table touched by an RLS migration must be both enabled and forced when the complete migration run finishes. An enforced table without an applicable policy denies row access. Existing SQL privileges still determine which commands the connection can execute; policies restrict rows within those privileges.
A filtered read, update or delete may affect zero rows. A rejected insert or new value produces PostgreSQL SQLSTATE 42501. Check the affected-row count and verify that a denied write left its target unchanged.
Pass a verified application identity
Authenticate before opening the RLS transaction. Use your application’s server session API, such as Better Auth or your own WorkOS integration, and reject missing, tampered, expired or revoked sessions. A decoded token, browser-supplied user ID or ohmyho.st hosting login does not establish an application identity. Preserve the supported framework and auth integration for your app. The following route code assumes thatsession has already been verified on the server and env contains the actual Worker binding:
subject is nonempty text, so UUIDs and provider-specific user IDs can both be used. The platform installs two SQL helpers:
Use
database.withRls(null, callback) to select anonymous access explicitly. The platform derives role as authenticated for an identity or anon for null. Callers cannot override sub or role. An ordinary query outside withRls receives no implicit application user.
The callback receives only query(). The service owns the transaction, sets the context locally and clears it when the transaction ends. Do not issue BEGIN, COMMIT, ROLLBACK, savepoints or session-control commands. A query or result error prevents commit even if your callback catches it. Already-started queries finish before successful completion; saved transaction handles cannot be used afterwards.
Keep the scope short: at most 100 statements, 30 seconds total and five seconds idle, with the existing query and result limits. Finish database work before waiting for email, uploads or an external provider. If commit confirmation is lost, database_transaction_outcome_unknown is nonretryable: inspect the business outcome before deliberately retrying.
Use trusted authorization claims
Derive organization membership and administrator permissions from server-controlled authority. Do not copy freely editable profile metadata into authorization claims. For example, after your backend checks an active membership, it can pass the checked organization ID:organization_id column can use this condition in both USING and WITH CHECK:
service_role bypass. Give them an explicit trusted identity and policies for the work they need. PostgreSQL’s session context is an assertion by your backend; it is not cryptographic token verification and cannot protect against a compromised backend or arbitrary SQL injection. Continue to parameterize SQL and verify sessions before choosing an identity.
Use Kysely in the same transaction
The optional adapter uses the already-open RLS transaction:destroy() releases the adapter’s wrappers; withRls still owns commit or rollback. The existing private dialect remains available for unprotected work and authentication/session lookup.
Change policies through migrations
Native RLS migrations supportCREATE POLICY, ALTER POLICY conditions, and atomic DROP POLICY/recreation, with permissive or restrictive rules for ALL, SELECT, INSERT, UPDATE and DELETE. Name the target table explicitly in public or private. It must be an ordinary table owned by the application migrator. Use TO PUBLIC or omit the role.
Multiple permissive policies combine with OR. Restrictive policies add AND requirements and need an applicable permissive policy. Review these combinations when adding a rule; a new permissive policy can widen access.
Append a new migration for each change. Never edit an applied file. All pending files apply atomically. DISABLE ROW LEVEL SECURITY, NO FORCE, policy renames, CASCADE, role/grant changes and ordinary destructive schema changes are unsupported. The ohmyhost schema is reserved; use only its two provided helpers in policies and do not create or replace platform objects.
Adopt RLS on an existing app
- Deploy and verify application code that authenticates users and sends all protected-table operations through
withRls. Keep the tables unenforced during this first step. - Check every active consumer of the physical database, including background jobs. For shared Dev/Prod data, both active applications must be ready immediately before enforcement.
- Add policies and
ENABLE/FORCEin a new migration, then verify existing records and allowed/denied CRUD for two users and anonymous access. - Keep a tested, context-ready application version as the rollback target. After enforcement, an older context-free build is not a supported rollback; policies are not automatically disabled to restore access.