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 step —
first / 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.
Problem
ClerkClaimSchemain@fireproof/core-types-basedeclaresparams.first,params.last,params.image_url, andparams.nameas required strings:But Clerk's JWT template variables for these attributes resolve to
undefinedwhen the user hasn't filled in the corresponding field. This happens routinely for:last_nameby default on subsequent sign-insfirst/last/image_urlall absentThe schema then rejects the JWT with:
Downstream effect:
ClerkApiToken.decodeandClerkApiToken.verifyboth bail before returning the claim, so any product gating ongetTokenClaims()(e.g. vibes.diy's first-prompt flow) silently fails for the affected user.Reproduction
Suggested fix
Wrap the four cosmetic profile fields in
.catch(...)so missing template variables fall through to safe defaults instead of erroring out, while keepingemail/email_verifiedstrict:z.infer<typeof ClerkClaimSchema>is unchanged (.catchpreserves the inner output type), so no downstream type drift.Workaround in vibes.diy
We're currently shipping a pnpm patch of
0.24.19that 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.lastabove the visibility threshold.