Skip to content
Chapter 57Lesson 7

Quiz - Roles, action wrappers, and the audit trail

Quiz progress

0 / 0

Of the three checks every privileged Server Action owes — valid session, sufficient role, parsed input — the lesson argues the role check is the dangerous one to leave inline in the action body. Why is that check the one most likely to ship missing?

Nothing downstream depends on it: drop it and the code still compiles, still runs, still looks right. The missing session is noticed because the action obviously needs a user, and the missing parse fails to type-check below it — but an unused role check leaves no trace.

The role check is the slowest of the three, so developers delete it to speed the action up and forget to put it back.

TypeScript can’t represent roles, so the role check can only ever be a runtime string comparison the compiler ignores.

Two claims about where a role lives and when it’s safe to trust. Select every statement that is correct.

The role belongs on the member row, keyed by (orgId, userId), so the same person can be an owner in one org and a plain member in another.

requireOrgUser should read the role fresh from the database each request, so a demotion takes effect within seconds rather than waiting for the session to refresh.

Storing the role as isAdmin: true on the user record is fine, since it travels with the person everywhere they go.

Once baked into the session cookie at sign-in, the role is safe to trust until the user signs out.

A GET /api/invoices/:id runs through authedRoute. The caller has a valid session and a sufficient role, but the id names an invoice that belongs to a different org, so the tenant-scoped read returns nothing. What should the handler send back?

404 Not Found — to this caller the row doesn’t exist, which reveals nothing about whether it exists for someone else.

403 Forbidden — the caller is authenticated but isn’t allowed to see this particular row.

200 OK with an empty body — the query didn’t error, so the request technically succeeded.

The member-management actions write the member row directly through Drizzle inside withTenant, instead of calling Better Auth’s auth.api.removeMember / updateMemberRole. What’s the reason for owning the write?

Better Auth’s org methods run their after hooks after their internal transaction has committed, so the audit row would land in a different transaction than the membership change — breaking the “audit row exists iff the work landed” contract.

auth.api calls are much slower than a direct Drizzle write, and member management is a latency-sensitive path.

Better Auth refuses to write the member table when row-level security is enabled, so a direct Drizzle write is the only option.

A bug slips past review and a request handler, connected as the app role, actually fires UPDATE audit_logs SET payload = … at Postgres. Of the three append-only layers, which one stops the rows from changing at that moment?

The deny-update RLS policy — its USING (false) predicate matches no row, so the UPDATE touches zero rows.

The tx: Transaction type on logAudit, which rejects the call before it runs.

The absence of an updated_at column, which leaves the query no field to write to.

Your api_keys table stores each key’s prefix and keyHash, and the create action returns the raw prefix.secret to the admin exactly once. A teammate pushes back: hash the secret with bcrypt — “the same slow hash we trust for passwords” — and keep the raw key encrypted so it can be shown again if someone loses it. What’s the senior response?

Store only sha256(secret) and show the raw key once: a stolen api_keys table then yields nothing usable. A fast hash is right precisely because the secret is 32 bytes of CSPRNG entropy — unguessable at any hash speed — so bcrypt’s deliberate slowness, which exists to protect low-entropy human passwords, buys nothing here and only taxes every verify.

Use bcrypt — any credential stored in the database deserves the same slow hash a password gets, no matter how the credential was generated.

Keep the raw key encrypted at rest so it can be re-displayed later; losing a key shouldn’t force the user to mint a new one.

Quiz complete

Score by topic