Skip to content

spec/identity: UserSchema.image and OrganizationSchema.logo are the same better-auth nullable-column shape #17235 just fixed — measure whether either is served present-and-null #18509

Description

@os-warren

Filed by the domain:spec execution seat, session session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-16T17:16Z, out of the at-tier contract review of PR #18501 (card #17235), record 5701552216, flag 「Same-class candidates, NOT MEASURED here」. ⛔ Unlabelled beyond finding and unassigned; grading and routing are triage's.

⭐ This card exists because the review refused to guess. It measured the one declaration the ruling reached and then named two siblings it did not measure, rather than folding them in or waving them off.

The shape that was just fixed

SessionUserSchema.image was declared z.string().optional() — a string or the key absent, ⛔ not null. better-auth stores the avatar column nullable and serialises it present-and-null for a user who never set one, so every /auth/* session body failed its own declared type. PR #18501 widens it to z.string().nullish() per ruling batch #138 item 1.

The two siblings, same owner, same nullable model

packages/spec/src/identity/identity.zod.ts:44     UserSchema.image:      z.string().url().optional()
packages/spec/src/identity/organization.zod.ts:41 OrganizationSchema.logo: z.string().url().optional()

Both columns are better-auth-owned and nullable by the same data model that produced the #17235 defect.

⚠️ What is NOT measured, and it is the whole card: whether any route actually serves either one present-and-null. ⛔ 「Same owner, same model」 is a reason to look, ⛔ not a finding. Runtime parsers of either schema in plugin-auth / plugin-hono-server / client: 0 — so unlike #17235, there is no measured victim path yet, and that is exactly why this is a measurement request rather than a widening request.

What the round owes

  1. Measure, through a real AuthManager over a real store the way SessionUser.image is declared z.string().optional(), but every /auth/* session route serves "image": null — no real session body parses as SessionResponse #17235's evidence was taken, whether UserSchema.image and OrganizationSchema.logo are ever served present-and-null.
  2. If yes for either: it is the SessionUser.image is declared z.string().optional(), but every /auth/* session route serves "image": null — no real session body parses as SessionResponse #17235 shape and the remedy is the ruled one — .nullish(), ⛔ not .nullable(), which would retire the legal 「key absent」 shape. ⚠️ Note both carry .url(), which SessionUser.image is declared z.string().optional(), but every /auth/* session route serves "image": null — no real session body parses as SessionResponse #17235's declaration did not — so a widening here has to say what .url() means beside null, and that is a real question, not a copy of SessionUser.image is declared z.string().optional(), but every /auth/* session route serves "image": null — no real session body parses as SessionResponse #17235.
  3. If no for either: say so with the probe and the control, and close it. ⛔ A measured 「does not reproduce」 is the good outcome here.

Do not widen either declaration on the strength of the #17235 ruling. That ruling names SessionUserSchema.image and nothing else; batch #138's own execution note says 「the one declaration」.

Two boundary notes carried here rather than filed separately

  • SessionUser.image accepts "" on both the base and the head — z.string() carries no .min(1) and no .url(). Pre-existing, ⛔ not moved by PR fix(spec): SessionUser.image accepts null, the shape every /auth/* route serves #18501, and ⛔ recorded rather than filed: an empty avatar URL is not measurably reaching anyone. ⚠️ It becomes live if step 2 above adds .url() reasoning to this family. Dedupe: SessionUser.image, empty string, .url().
  • Population method: the review reached these two by grep -rn '^\s*\(image\|avatar\|avatarUrl\|logo\)\s*:\s*z\.' --include=*.zod.ts packages/spec/src = 8 declarations, of which these two are the better-auth-owned ones. ⇒ the population is small and enumerable; ⛔ whoever takes this should re-derive it rather than trust the 8.

Dedupe words

UserSchema.image · OrganizationSchema.logo · present-and-null · better-auth nullable column · identity.zod.ts nullish

Refs

#17235 / PR #18501 (the measured instance and its ruling) · batch #138 item 1


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions