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.
Background
readers.id(and any FK referencing it, e.g.memberships.readerId) is aBetter Auth-generated opaque text ID (e.g.
6QWDKetmN3wWTimN1d0SHBYPPxoCYp4Y),not a Snowflake
EntityId(/^[1-9]\d{0,18}$/). Most of the codebaseassumes 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, andMembershipControllerranreaderIdthroughparseEntityId/zEntityId, which rejects every real Better Auth reader id(they're never pure-digit). This broke, for effectively all readers:
POST /membership/checkoutGET /membership/statusPUT/DELETE /membership/members/:readerId(admin manual grant/revoke)CommentController.withMembership(
getRecentlyComments,getCommentsByRefId, etc.) — this is whatoriginally surfaced the bug via
Invalid EntityId format: ...in the logs.Hotfixed by treating
readerIdas a plain string throughout the membershipmodule, matching the pattern already used in
comment.repository.tsfor thesame 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.:
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
ReaderId(orAuthId) type, separate fromEntityId, with its own parse/validate helpers — so a future accidentalparseEntityId(readerId)call fails to compile instead of throwing atrequest time.
Scope to check
refText-declared FK columns that actually point at BetterAuth-owned tables vs. genuine Snowflake entities.
numeric Snowflake ids (grep for
EntityId/parseEntityId/zEntityIdnear
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.