Security boundary
Understand token storage, host authorization, audience checks, and atomic grants.
Component responsibilities
convex-invite generates cryptographically random tokens, persists only their
versioned SHA-256 digests, enforces lifecycle transitions, compares audience
references, and prevents replay or conflicting acceptance.
Raw tokens are returned only by issue and resend. They never appear in
management results, exports, delivery summaries, or component logs.
Host responsibilities
Every host wrapper must:
- authenticate the caller;
- authorize the requested scope and resource;
- normalize and verify audience ownership;
- rate-limit token-bearing public endpoints;
- send messages without logging tokens;
- create the resulting membership or domain grant transactionally.
Bearer-token URLs are sensitive
Avoid analytics and third-party requests on token-bearing pages. Use a strict
Referrer-Policy and scrub the token from browser history as soon as
practical.
Atomic acceptance
Acceptance and the resulting host grant belong in one top-level Convex mutation. If membership creation throws, Convex rolls back both writes. Do not accept in an action and create access later.
const grant = await invites.accept(ctx, {
token,
acceptedBy: identity.subject,
audienceRef: verifiedNormalizedEmail(identity),
});
const membershipId = await ctx.db.insert("memberships", {
subject: identity.subject,
resourceRef: grant.resourceRef,
role: grant.role,
});
await invites.setAcceptanceResult(ctx, {
scope: grant.scope,
invitationId: grant.invitationId,
acceptedBy: identity.subject,
result: { membershipId },
});