v0.28.0
@kitcn/resend
Published @kitcn/resend@0.28.0.
kitcn
Minor Changes
-
#430
d90c209Thanks @zbeyens! - ## Breaking changes- Require Better Auth 1.7. Existing deployments need a maintenance window and
two schema deployments. Do not refresh the required Better Auth 1.7 schema
first: the old schema rejects new fields, while the required schema rejects
old rows.
// Before const account = { accountId, providerId, userId }; // After const account = { accountId, issuer, providerId, userId };
Deployment 1: optional fields and backfills
Stop authentication writes, background jobs, and admin APIs that write
account,team, orteamMember. Keep them stopped through deployment 2.Keep the currently deployed Better Auth version. Temporarily add
issuerand
the lookup index to the existing account schema owner. Apps using organization
teams must also add optionalmemberCountandmembershipKeyfields plus the
membershipKeyindex:// ORM account field/index issuer: text(), index("accountId_issuer").on(accountTable.accountId, accountTable.issuer), // ORM team and teamMember fields/index memberCount: integer(), membershipKey: text(), uniqueIndex("membershipKey").on(teamMemberTable.membershipKey), // Raw Convex account field/index issuer: v.optional(v.string()), .index("accountId_issuer", ["accountId", "issuer"]) // Raw Convex team and teamMember fields/index memberCount: v.optional(v.number()), membershipKey: v.optional(v.string()), .index("membershipKey", ["membershipKey"])
membershipKeystays optional in the generated Better Auth 1.7 schema, and
the 1.7 adapter falls back to the existing(teamId, userId)pair when it is
absent. Existing rows do not need a membership-key backfill; new 1.7 writes
populate it.Create a migration with
bunx kitcn migrate create backfill_account_issuer.
Inventory every provider and resolve both parts of its 1.7 identity from
trusted provider data. Credential accounts uselocal:credentialand their
linked user ID. OAuth providers without an issuer use
local:oauth:${encodeURIComponent(providerId)}and keep their stable provider
subject unless the 1.7 provider contract changed it.Microsoft is a required exception: map every
microsoftand
microsoft-entra-idrow from its oldsubto the verified directoryoid
from a verified stored ID token or trusted Entra export. Apply the same rule to
custom OAuth/OIDC providers whose 1.7accountSubjectdiffers. Never derive an
identity from email or another mutable profile field. The
Better Auth 1.7 upgrade guide
owns the provider-specific mapping rules.import { defineMigration } from "kitcn/orm"; const issuerByProviderId = { credential: "local:credential", github: "local:oauth:github", google: "https://accounts.google.com", } as const; // Add every row whose trusted 1.7 provider subject differs from accountId. const accountIdByRowId: Record<string, string> = { "microsoft-account-row-id": "verified-directory-oid", }; export const migration = defineMigration({ id: "20260826_000000_backfill_account_issuer", up: { table: "account", migrateOne: async (ctx, account) => { const issuer = issuerByProviderId[ account.providerId as keyof typeof issuerByProviderId ]; if (!issuer) { throw new Error(`Map issuer for provider ${account.providerId}`); } const mappedAccountId = accountIdByRowId[account._id]; if ( (account.providerId === "microsoft" || account.providerId === "microsoft-entra-id") && !mappedAccountId ) { throw new Error(`Map verified Microsoft oid for ${account._id}`); } const accountId = mappedAccountId ?? (account.providerId === "credential" ? account.userId : account.accountId); if (!accountId) { throw new Error(`Map account subject for ${account._id}`); } if (account.issuer !== undefined && account.issuer !== issuer) { throw new Error(`Issuer mismatch for account ${account._id}`); } if (account.issuer === issuer && account.accountId === accountId) { return; } const collision = await ctx.db .query("account") .withIndex("accountId_issuer", (query) => query.eq("accountId", accountId).eq("issuer", issuer) ) .unique(); if (collision && collision._id !== account._id) { throw new Error(`Duplicate account identity ${issuer}:${accountId}`); } return { accountId, issuer }; }, }, });
Apps using organization teams must also create a migration that sets every
team's count from the indexedteamMemberrows:export const teamMemberCountMigration = defineMigration({ id: "20260826_000001_backfill_team_member_count", up: { table: "team", migrateOne: async (ctx, team) => { const members = await ctx.db .query("teamMember") .withIndex("teamId", (query) => query.eq("teamId", team._id)) .collect(); return { memberCount: members.length }; }, }, });
Deploy the optional schema and migration, then require a completed status:
bunx kitcn codegen bunx kitcn deploy --prod bunx kitcn migrate status --prod
Raw Convex apps use the same resolver and indexed collision check in a
paginated internal mutation after deploying the optional fields and indexes.
Team users must also countteamMemberrows by theteamIdindex and patch
every team using the team's Convex_id. Finish every page, verify no account
lacks either identity field, verify no(issuer, accountId)collision exists,
and verify every team count before continuing.Deployment 2: required Better Auth 1.7 schema
After the backfill is complete, upgrade KitCN and Better Auth, refresh the
auth-owned schema, and deploy the required field and compound identity index:# Default KitCN schema owner bunx kitcn add auth --schema --yes # Raw Convex schema owner; use this command instead of the default command bunx kitcn add auth --preset convex --yes bunx kitcn deploy --prod
Verify returning credential, OAuth, and Microsoft sign-in plus team membership
changes against the migrated data. Resume writes only after these checks pass.Features
- Support Better Auth 1.7 account identity constraints, declared table indexes,
atomic adapter mutations, stable join configuration, session hydration, and
organization metadata reads.
Patches
- Fix OpenID discovery to advertise the configured JWT signing algorithm.
- Require Better Auth 1.7. Existing deployments need a maintenance window and