Skip to content
Chapter 56Lesson 5

Quiz - Organizations as the tenancy model

Quiz progress

0 / 0

A founder reviews their consultancy’s invoices in one browser tab while triaging Acme’s in another — two tabs, two different active orgs, same person. This is the deciding reason activeOrganizationId lives on the session row rather than the user row. What breaks if you put it on user instead?

Both tabs share one slot, so switching org in one tab silently changes which company the other tab is operating inside.
The active org would survive sign-out, so the next person to sign in on that machine inherits the previous user’s company.
Better Auth’s setActive can only write to the session table, so the value on user would never update.

Your switch-org Server Action calls auth.api.setActiveOrganization(...) and returns. Switching to Acme works, but the dashboard keeps streaming the previous org’s invoices until you happen to reload. What’s missing?

revalidatePath('/', 'layout') after the switch — the cached layout and pages were computed under the old org and are now stale.
A shorter cookie-cache maxAge — the decoded session is cached for a few minutes, so the new org isn’t visible until that window lapses.
A second getSession call after the switch to force the new activeOrganizationId to be re-read from the database.

The whole point of tenantDb(orgId) is summed up by one uncomfortable sentence. Which framing is the one the lesson insists on?

Forgetting the org filter stops being something you can express — the unscoped call shape no longer typechecks.
If you forget the org filter, the helper notices the missing predicate and adds it back for you at runtime.
The helper logs a warning whenever a tenant query runs without an org predicate, so review catches the leak before it ships.

A genuine cross-org need arrives: a nightly job that rolls up revenue across every organization. A teammate proposes adding a tenantDb({ allOrgs: true }) mode so the same helper covers it. Why does the lesson reject that and route the job through the raw db in a separate scripts/ file instead?

A built-in bypass turns “reach for a different, scary client” into “pass a flag,” and the guarantee erodes one innocent flag at a time; a separate client in a separate file keeps every cross-org read a visible, deliberate decision.
tenantDb is typed to a single orgId string and can’t be widened to accept a boolean flag without breaking the mapped-type registry.
Cross-org reads are slower, and isolating them in scripts/ keeps the request-path bundle from importing the heavier unscoped query builder.

Your audit_logs table now has an RLS policy. A teammate suggests dropping the tenantDb scoping for that table — “the database enforces isolation now, the app filter is redundant.” Are they right?

No — on an RLS table the app still scopes with tenantDb; the two are independent layers, and removing one means a single bug (a policy typo, a forgotten FORCE) can leak.
Yes — the policy runs on every query and every connection, so the application-layer filter genuinely adds nothing once RLS is on.
No — and the reason is defense in depth: a bug in the app is caught by the policy, a bug in the policy is caught by the app, so a leak now has to defeat both layers at once.

Three triggers can push a table past application-layer scoping into “this one earns RLS.” Which of these is a real trigger from the lesson? Select all that apply.

The data is highest-stakes — one missed scope is a regulatory or legal incident, not just a bug (PHI, financial PII, subpoenable audit logs).
The table is read or written by paths the request-path helper can’t span — admin tools, batch jobs, external integrations holding DB credentials.
The table is large enough that adding an org predicate to every query measurably slows reads.
The app server could be compromised and made to set any org id it wants.

The withTenant helper sets the tenant id inside an explicit transaction:

await tx.execute(sql`select set_config('app.org_id', ${orgId}, true)`);

Two choices here are load-bearing. Which reasoning is correct?

The true third argument makes it transaction-local (like SET LOCAL) so it clears on commit and can’t leak to the next request on a pooled connection; set_config is used over raw SET LOCAL because it accepts orgId as a bind parameter.
The true argument enables RLS on the connection, and set_config is used because raw SET can’t run inside a transaction.
The transaction exists to roll back the audit write if the policy rejects it, and set_config is interchangeable with SET — either works on a pooled connection.

drizzle-kit generate emits the RLS migration. Before shipping, you read the generated SQL — which the lesson calls a reflex, not a suggestion. What’s the specific thing the codegen leaves out that you must add by hand?

ALTER TABLE audit_logs FORCE ROW LEVEL SECURITY — codegen emits ENABLE but not FORCE, so the table owner (and owner-run tests and migrations) silently bypasses the policy until a non-owner request hits it.
The WITH CHECK predicate — codegen only emits USING, so inserts for the wrong org would slip through until you add the write filter manually.
The current_setting('app.org_id', true) call — codegen hardcodes a literal org id that you must replace with the session-variable lookup.

Quiz complete

Score by topic