Skip to content

ClerkClaimSchema rejects new signups missing optional Clerk profile fields (last_name etc.) #1814

Description

@jchris

Problem

ClerkClaimSchema in @fireproof/core-types-base declares params.first, params.last, params.image_url, and params.name as required strings:

// fp-clerk-claim.zod.ts
export const ClerkEmailTemplateClaimSchema = z.object({
  // ...
  first: z.string(),
  image_url: z.string(),
  last: z.string(),
  name: z.string().nullable(),
  // ...
});

But Clerk's JWT template variables for these attributes resolve to undefined when the user hasn't filled in the corresponding field. This happens routinely for:

  • Apple Sign In — Apple hides last_name by default on subsequent sign-ins
  • Single-name users — Google OAuth profiles with no last name
  • Email signups without a profile stepfirst / last / image_url all absent

The schema then rejects the JWT with:

ZodError: [{
  expected: "string",
  code: "invalid_type",
  path: ["params", "last"],
  message: "Invalid input"
}]

Downstream effect: ClerkApiToken.decode and ClerkApiToken.verify both bail before returning the claim, so any product gating on getTokenClaims() (e.g. vibes.diy's first-prompt flow) silently fails for the affected user.

Reproduction

import { ClerkClaimSchema } from "@fireproof/core-types-base";

// JWT-shaped claim from a brand-new Apple SSO signup
const claim = {
  azp: "https://vibes.diy",
  exp: 1900000000,
  iat: 1899999400,
  iss: "https://clerk.vibes.diy",
  params: {
    email: "newuser@example.com",
    email_verified: true,
    first: "Pat",
    image_url: "https://img.clerk.com/...",
    // last: undefined  ← Clerk template var resolves to undefined
    name: null,
    public_meta: {},
  },
  role: "user",
  sub: "user_2abc",
  userId: "user_2abc",
};

const result = ClerkClaimSchema.safeParse(claim);
// result.success === false
// result.error.issues[0] === { code: "invalid_type", path: ["params","last"], ... }

Suggested fix

Wrap the four cosmetic profile fields in .catch(...) so missing template variables fall through to safe defaults instead of erroring out, while keeping email / email_verified strict:

export const ClerkEmailTemplateClaimSchema = z.object({
  // ...
  first: z.string().catch(""),
  image_url: z.string().catch(""),
  last: z.string().catch(""),
  name: z.string().nullable().catch(null),
  // ...
});

z.infer<typeof ClerkClaimSchema> is unchanged (.catch preserves the inner output type), so no downstream type drift.

Workaround in vibes.diy

We're currently shipping a pnpm patch of 0.24.19 that applies exactly the diff above. Happy to drop it once an upstream release lands.

Why this surfaced now

The schema-gated path has been live since Feb in vibes.diy and presumably others; what changed is the mix of inbound signups widened (Apple SSO + email-only paths grew), pushing the share of JWTs with undefined params.last above the visibility threshold.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions