Skip to content

Normalize reader/auth IDs to Snowflake EntityId (or split a distinct branded type) #2774

Description

@Innei

Background

readers.id (and any FK referencing it, e.g. memberships.readerId) is a
Better Auth-generated opaque text ID (e.g. 6QWDKetmN3wWTimN1d0SHBYPPxoCYp4Y),
not a Snowflake EntityId (/^[1-9]\d{0,18}$/). Most of the codebase
assumes every ID crossing a repository boundary is a Snowflake bigint
serialized as text, validated via parseEntityId / toEntityId / zEntityId.

This mismatch already caused a real bug: MembershipRepository,
EntitlementService, and MembershipController ran readerId through
parseEntityId / zEntityId, which rejects every real Better Auth reader id
(they're never pure-digit). This broke, for effectively all readers:

  • POST /membership/checkout
  • GET /membership/status
  • PUT/DELETE /membership/members/:readerId (admin manual grant/revoke)
  • Any comment endpoint touching CommentController.withMembership
    (getRecentlyComments, getCommentsByRefId, etc.) — this is what
    originally surfaced the bug via Invalid EntityId format: ... in the logs.

Hotfixed by treating readerId as a plain string throughout the membership
module, matching the pattern already used in comment.repository.ts for the
same FK. This issue tracks the follow-up so the same class of bug can't
recur elsewhere.

Proposal

Audit every ID crossing the readers/auth boundary and pick one consistent
representation, e.g.:

  • Migrate readers.id (and the other Better Auth-owned tables: accounts,
    sessions, apiKeys, passkeys, verifications) to Snowflake-generated IDs, so
    they're consistent with the rest of the schema, or
  • Introduce a distinct branded ReaderId (or AuthId) type, separate from
    EntityId, with its own parse/validate helpers — so a future accidental
    parseEntityId(readerId) call fails to compile instead of throwing at
    request time.

Scope to check

  • All refText-declared FK columns that actually point at Better
    Auth-owned tables vs. genuine Snowflake entities.
  • Any other module besides membership assuming reader/session/user ids are
    numeric Snowflake ids (grep for EntityId / parseEntityId / zEntityId
    near readerId / userId / reader-auth code paths).

Not in scope here

This is a larger, deliberately deferred refactor. The immediate hotfix for
the membership module ships separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions