By Chason ArthurRevision 145.021Published Updated

API security / PostgreSQL RLS / Identity

Build the safeguards.

Follow Tenant Boundary Lab from its visible failure to a testable implementation. Each step names one decision and a checkpoint.

  1. Draw the resource boundary
  2. Validate the session lifecycle
  3. Authorize the action as well as the account
  4. Derive tenant context from trusted membership
  5. Use a restricted database role
  6. Apply the row policy
  7. Scope connection state to the transaction
  8. Make denials observable without exposing data

Step 1 of 8

Draw the resource boundary

The fixture has two companies and one document per company. An Orchard account has no Harbor membership. A document identifier locates a resource; it does not grant permission to use it. Make that distinction explicit in the API design and in tests.

GET /documents/22
# Identity: Orchard editor; document owner: Harbor

Checkpoint: Identify the expected company before reading the query.

Step 2 of 8

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

Checkpoint: Confirm that the expired safe path executes no document query.

Step 3 of 8

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

Checkpoint: Explain why hiding an Edit button is insufficient.

Step 4 of 8

Derive tenant context from trusted membership

The selected company must belong to the verified account. A request body or query parameter cannot simply declare its tenant. The native adapter stores fictional account memberships server-side and rejects unlisted request fields; it never uses a submitted tenant to scope the database.

session.memberships.includes(session.tenant)

Checkpoint: Name the server-side source of the company value.

Step 5 of 8

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;

Checkpoint: Locate the role change in the displayed source.

Step 6 of 8

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

Checkpoint: Run the unfiltered list and verify that Harbor remains invisible.

Step 7 of 8

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

Checkpoint: Explain the difference between the browser’s fresh database and a long-lived connection pool.

Step 8 of 8

Make denials observable without exposing data

The API returns a missing-record response for an inaccessible document, matching an absent identifier. Internal audit records can explain a denial without returning the forbidden payload. The exercise reports status and row counts so the boundary is visible; real logs also need careful treatment of identifiers and document content.

assert.equal(foreignRows, 0);
assert.equal(status, 404);

Checkpoint: Test direct HTTP requests rather than relying on the UI’s available controls.

Practice the interview explanation

  1. Which fields come from the verified session, and which are controlled by a requester?
  2. What happens if a handler omits a WHERE clause?
  3. Which database roles bypass RLS, and how would you test the production connection role?

Use the experiment as an example. State the failure, explain the chosen boundary, show the test and name the tradeoff. Keep measured results separate from production assumptions.

Read the primary documentation

Mux’s full-stack opening includes identity and account-management product features.

Related role from the job review

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

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 →