Quiz - Organizations as the tenancy model
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?
setActive can only write to the session table, so the value on user would never update.session row, not the shared user row. Putting it on user collapses every device onto one slot and makes a switch in one tab yank the rug out from under the others. (Sign-out clears the session either way, and setActive writing the session row is the mechanism, not the reason.)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.maxAge — the decoded session is cached for a few minutes, so the new org isn’t visible until that window lapses.getSession call after the switch to force the new activeOrganizationId to be re-read from the database.setActive already rewrites the cookie immediately, so the cookie-cache window is irrelevant here — the stale data comes from the route cache, which revalidatePath('/', 'layout') clears (with router.refresh() on the client completing the picture).The whole point of tenantDb(orgId) is summed up by one uncomfortable sentence. Which framing is the one the lesson insists on?
tenantDb doesn’t “save you when you forget”; it removes the unscoped call shape from the surface entirely, so the missing filter is a type error, not a runtime rescue and not a review checklist item. Optional correctness is the bug.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?
tenantDb is typed to a single orgId string and can’t be widened to accept a boolean flag without breaking the mapped-type registry.scripts/ keeps the request-path bundle from importing the heavier unscoped query builder.db import the loud, reviewable signal that you’re in cross-org territory. The type system could be made to accept a flag — the objection is about discipline, not feasibility.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?
tenantDb; the two are independent layers, and removing one means a single bug (a policy typo, a forgotten FORCE) can leak.tenantDb, it doesn’t replace it. The value is in the independence: the helper catches the 99% request-path case, the policy catches what the helper can’t span (raw SQL, external writers), and each layer catches the other’s bugs. Drop either and you’re back to a single point of failure on data where one leak is unrecoverable.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.
SET any org id and read freely; that’s a least-privilege / DB-role problem for a later chapter.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?
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.true argument enables RLS on the connection, and set_config is used because raw SET can’t run inside a transaction.set_config is interchangeable with SET — either works on a pooled connection.set_config(name, value, is_local) with is_local = true is the function form of SET LOCAL: scoped to the transaction, auto-cleared on commit, so a pooled connection returns clean. It’s preferred over raw SET LOCAL precisely because a runtime value like orgId must be a bound parameter, never string-concatenated. Plain SET (session-scoped) is the footgun — it persists across requests on the same connection and leaks one tenant into the next.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.WITH CHECK predicate — codegen only emits USING, so inserts for the wrong org would slip through until you add the write filter manually.current_setting('app.org_id', true) call — codegen hardcodes a literal org id that you must replace with the session-variable lookup.ENABLE ROW LEVEL SECURITY turns the policy on but leaves the table owner bypassing it — and migrations, admin tooling, and naive tests all run as the owner. So an owner-run test passes for the wrong reason and the protection isn’t real until a paying customer’s non-owner request exercises it. drizzle-kit has no force option, so you add FORCE in a follow-up --custom migration and verify it in the generated SQL.Quiz complete
Score by topic