Skip to content

audit: a user's FIRST session predates their membership, so every audit row written in that window carries a NULL tenant and is invisible to RLS readers #8245

Description

@os-zhuang

Observation-class finding, measured while implementing #8144 (PR #8244). Not caused by that change — it is a pre-existing ordering property that #8144 made visible because it put a row on the ledger at exactly that moment.

What was measured

databaseHooks.session.create.before derives a session's activeOrganizationId from the caller's sys_member row (ADR-0081 D1, AuthManager.composeDatabaseHooksdefaultActiveOrg). The membership itself is written by the ADR-0093 reconciler composed into user.create.after, and better-auth defers that past the signup transactionmembership-actor-attribution.dogfood.test.ts already polls up to 10s for it, and calls the delay out in a comment.

So the session minted by sign-up is created before the user has any membership. It carries activeOrganizationId: null, legitimately: at that instant there is no organization to name.

Reproduced while writing #8144's dogfood test: signing up a member and immediately reading the login audit row for that first session yields tenant_id: null (and organization_id: null). Waiting for the sys_member row before signing in produces a correctly tenanted row on every subsequent session. The test now measures the tenant half on an ordinary sign-in and documents the first-session case rather than asserting on it.

Why it is worth a card

The SecurityPlugin's RLS predicate gates on organization_id. audit-writers.ts already names the consequence in its own comment, for the sibling cause:

Without this fallback, audit rows are written with organization_id=NULL and the SecurityPlugin's RLS predicate (organization_id = current_user.organization_id) hides them forever — making the audit log UI appear permanently empty even though writes succeed.

Its fallback reads the audited record's own organization_id, which cannot help here: a session row has no organization column either, and the account/user rows being written in that same window are in the same state.

The window is narrow (a user's first session) but the loss is permanent — nothing back-fills a written ledger row — and the affected rows are exactly the ones describing account creation.

Related, not duplicate

This card is about the ordering that produces the no-active-org state for every new user, and about ledger rows specifically.

Not proposed here

Whether the fix is to settle the membership before minting the first session, to back-fill the tenant on the audit row when the session later gains one, or to accept it and document it, is a design call with a real cost on the sign-up hot path. No fix is implied by this filing.

Source: #8144 / PR #8244, packages/qa/dogfood/test/auth-session-audit-trail.dogfood.test.ts (see the beforeAll note).

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions