By Chason ArthurRevision 145.021Published Updated

API security / PostgreSQL RLS / Identity

You’re signed in. Whose data is this?

A signed-in account requests another company’s document. A viewer attempts an edit. A developer forgets a filter. See which layer should stop each request.

The idea in plain language

Authentication establishes who is making a request. Authorization determines whether that identity may perform this action on this resource. A company boundary must survive a changed identifier and a forgotten query filter.

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.

Your independent testing environment

Make the failure visible.

Start with the cross-company document request. The broken handler returns Harbor’s record to an Orchard account; the safeguarded path returns no visible record. Then try the viewer, expired-session and omitted-filter cases.

An Orchard session requests document 22, which belongs to Harbor.

Ready. Choose an example and run both implementations.

Explain the result

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.

Open the exact source behind this edition. The viewer checks each file against its SHA-256 manifest and explains the decision it implements.

Download this lab

Choose your next step

Build the safeguards

Eight explained steps, a source example and a checkpoint at each decision.

Read the tutorial

Compare the alternatives

Understand where each pattern helps and which tradeoffs remain.

Compare approaches

Run and extend it locally

Download the independent environment, run the native checks and try a specific extension.

Open the how-to guide

Before you try it

Do I need to know how to code?

Start with Run and read the comparison. The tutorial introduces the code after you have seen the behavior. The how-to guide separates a no-install route from the native developer exercise.

What happens to the experiment when I reset?

Reset discards this lab’s temporary state and cancels or terminates its pending work. These examples use invented records. No account, production data or cloud deployment is needed.

What does the browser experiment establish?

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.

Lab discussion / Tenant Boundary Lab

What would you try next?

Ask about the code, compare an approach, or describe an edge case. Reader comments are reviewed before publication.

All lab discussions

Reader comments

Open the discussion to read and add reader comments.

Read or write a comment →

Code walkthrough

Follow the implementation in code.

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.

Example 01 · Tutorial excerpt

Validate the session lifecycle

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};

Read the full tutorial step

Example 02 · Tutorial excerpt

Authorize the action as well as the account

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};
}

Read the full tutorial step

Example 03 · Tutorial excerpt

Use a restricted database role

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;

Read the full tutorial step

Example 04 · Tutorial excerpt

Apply the row policy

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))

Read the full tutorial step

Example 05 · Tutorial excerpt

Scope connection state to the transaction

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);

Read the full tutorial step