Skip to content

Sharing and access control

Access control is enforced in PostgreSQL with row-level security, not in application code. A query that should not see a row does not see it — regardless of which controller, endpoint, or trigger issued it.

That is the important property: a bug in a controller cannot leak records, because the filtering happens below the application entirely.

Ownership. Every record carries created_by. Creators see their own records.

Share rules. Model-level rules that grant access declaratively — “everyone in Support can read Issues”, “the assignee can write”. Evaluated automatically, and they are configuration, so they deploy with everything else.

Manual shares. A specific record shared with a specific user or group, the way you would share a document. Data, not configuration — they do not deploy.

The distinction matters: share_type is manual or rule, and a deploy replaces rule-derived shares while leaving manual ones alone.

share_permissionMeaning
readSee the record
writeSee and modify it
share_targetShared with
allEveryone in the workspace
userOne user
groupOne group, and its descendants

Groups are hierarchical, so granting to a parent grants to everything beneath it. That is usually what you want, and occasionally more than you meant — check the tree before sharing at a high level.

Share rules live in share_rules/ and move with polymesa deploy. Review them in a --dry-run like any other change: widening a rule is a data-exposure change, and it looks the same as any other diff line until you read it.

Plain sql runs as the current user with RLS applied — you get their rows, no filtering to write. This is why the default is safe.

// Only what this user may see
const { rows } = await sql.query<Issue>(`SELECT * FROM ${i}`);

sql.systemMode bypasses RLS completely:

// Every row, regardless of the caller
const all = await sql.systemMode.query<Issue>(`SELECT * FROM ${i}`);

Legitimate uses are operations with no user behind them — scheduled jobs, inbound email handlers, cross-workspace rollups.

Jobs have no triggering user, so plain sql sees almost nothing. They run as the built-in SYSTEM account and generally use systemMode deliberately. See Scheduled jobs.

polymesa query runs as your user with RLS applied, so exports are limited to what you can see. Two people running the same query can get different rows — that is correct, not a bug.