Build the safeguards
Eight explained steps, a source example and a checkpoint at each decision.
Read the tutorialAPI security / PostgreSQL RLS / Identity
Choose a pattern by the failure it addresses. Several safeguards can be necessary at once.
| Approach | Useful when | Decision or limitation |
|---|---|---|
| UI visibility | Make the available actions understandable | Not an authorization boundary |
| Handler policy | Validate identity, membership and action | Every entry point must invoke it |
| Database row policy | Restrict rows under a restricted application role | Owners, privileged roles and connection context need care |
| Role-based permissions | A small stable set of responsibilities | Can become coarse as rules depend on resources |
| Attribute-based policy | Rules depend on ownership or resource state | Requires explicit inputs and understandable audit reasons |
| Opaque server session | A local application owns session state | Needs lifecycle, transport and revocation controls |
| Environment | What you can do | Scope |
|---|---|---|
| This browser lab | Click, compare, reset and inspect exact source | Invented sessions feed actual PostgreSQL row policies in your browser. The download adds a local HTTP adapter with opaque fixture sessions. Neither version is a production identity provider or a test of every database-privilege configuration. |
| Independent download | Run the same prebuilt lab with Python’s local server | No package installation or automatic external calls needed for the prebuilt preview |
| Native exercise | Run language tests and the included local adapter or script | Requires the documented local toolchain; does not provision a cloud service |
The safe cross-company read returns 404 with no foreign rows. A viewer’s edit returns 403; an expired session returns 401. A list query without a tenant filter still returns only Orchard’s row under RLS.
Begin with an observable outcome, inspect the decisions that produced it, then test a repair. The companion tutorial explains the mechanism; the how-to guide takes the same source into an independent environment.
Mux’s full-stack opening includes identity and account-management product features.
Eight explained steps, a source example and a checkpoint at each decision.
Read the tutorialUnderstand where each pattern helps and which tradeoffs remain.
Compare approachesDownload the independent environment, run the native checks and try a specific extension.
Open the how-to guideLab discussion / Tenant Boundary Lab
Ask about the code, compare an approach, or describe an edge case. Reader comments are reviewed before publication.
Code walkthrough
These short excerpts connect the lab’s behavior to its implementation. Each example links to the full tutorial step for its context, tradeoffs and checkpoint. Names and dependencies belong to that source; the excerpts are not standalone programs.
The expired preset uses an already expired session. The safe policy checks the expiry before querying data. Production identity also needs a trusted issuer or session store, secure transport and revocation rules. Decoding an unverified token or accepting a browser role field would not establish identity.
if (!session || session.expires <= now) return {status: 401};
A viewer may read but not edit. Membership and action checks are separate: being in the right company does not make every action permissible. Deny an unsupported action before changing data, and test that the denial leaves the record unchanged.
if (action === "update" && session.role !== "editor") {
return {allowed: false, status: 403};
}
RLS is ineffective if the application routinely uses a bypassing role. The exercise explicitly changes from the owner to a restricted role before running the query. Policies, grants and owner behavior are different mechanisms. Test the exact role used by the application, including administrative paths.
CREATE ROLE lab_reader NOLOGIN NOSUPERUSER NOBYPASSRLS;
SET ROLE lab_reader;
The USING expression controls visible rows; WITH CHECK constrains new row values on supported writes. The example uses a session-derived company setting. The omitted-filter preset is important: a WHERE clause in one handler is not the only boundary the application should rely on.
USING (tenant_id = current_setting('app.tenant',true))
WITH CHECK (tenant_id = current_setting('app.tenant',true))
A real connection can be reused. The native adapter uses SET LOCAL ROLE and transaction-local set_config so the context expires when the transaction ends. It also serializes access to this single embedded connection. A production pool needs tests for concurrency, rollback and connection reuse.
SELECT set_config('app.tenant',$1,true);