Single-use invitation tokens with TTL enforcement and typed grants for Convex apps — issue, accept, revoke, and expire invite flows in sandboxed tables.
npm install @vllnt/convex-invitationsInvitations is the invite → accept → expire flow as a Convex component: a host mutation issues a single-use, expiring `token` via `issue`; the invitee redeems it with `accept`, which consumes the invite and returns an `InvitationGrant` the host applies — terminal states (`accepted`/`revoked`/`expired`) are final, so a replayed link can never double-grant. Typed end to end with `Invitations<TRole, TPayload>`, opaque `resourceRef`/`role`/`payload` kept host-defined via optional validators, and server-sourced time for TTL enforcement. Invite records live in the component's own sandboxed tables (zero blast radius into your schema), the host gates every management call (auth-agnostic by construction), and it runs identically on Convex Cloud and self-hosted convex-backend.
@vllnt/convex-invitations provides an `issue` mutation that inserts a pending invite and returns a single-use token plus an expiry timestamp. The host delivers the token out of band, and the invitee calls `accept` which consumes the invite atomically and returns the grant. Once accepted, the terminal state is final so replayed links cannot double-grant.
The component stores invites against an opaque `resourceRef` string, so it works for any resource type including SaaS organizations, game guilds, or publication teams. You call `invites.issue(ctx, orgId, { role: 'member' })`, get back a token, and on acceptance call `invites.accept(ctx, token, userId)` which returns the resourceRef and role for you to write into your own memberships table.
@vllnt/convex-invitations enforces TTL at accept time using server-sourced timestamps, not caller-provided values. Every invite carries an `expiresAt` field computed from the configurable `ttlMs` option. A daily cron also sweeps and prunes stale invites in bounded idempotent batches, and `peek` does an explicit read-time expiry check.
The component exposes a `revoke(ctx, token)` mutation that cancels a still-pending invite. The revoke and accept operations are designed so a race between the two yields exactly one winner, preventing a revoked token from being accepted concurrently.
@vllnt/convex-invitations is deliberately backend-only and does not send email. The `issue` mutation returns a token and `expiresAt`; the host application is responsible for delivering the token to the invitee, for example by constructing a URL and emailing it. This keeps the component domain-neutral and auth-agnostic.
No. @vllnt/convex-invitations enforces single-use semantics at the database level. Once an invite reaches an `accepted`, `revoked`, or `expired` terminal state, that state is final. A replayed token is rejected and will never produce a second grant.
The `Invitations` class accepts two generic parameters: `TRole` and `TPayload`. You also pass `roleValidator` and `payloadValidator` functions at construction time to narrow the values at the component boundary. For example, `new Invitations<'admin' | 'member', never>(components.invitations, { roleValidator: v.union(v.literal('admin'), v.literal('member')).parse })` enforces the role type on every issue and accept call.
Yes. The component stores invites in sandboxed tables scoped to the component instance, and its daily cron is designed to be correct under multiple named mounts. You can mount the component more than once in a single Convex app without cross-contamination between instances.
@vllnt/convex-invitations exposes `listPending(ctx, resourceRef, paginationOpts)` and `listByResourceState(ctx, resourceRef, state, paginationOpts)` as Convex queries, both of which return paginated results. Because they are standard Convex queries, you can use `useQuery` in your React client to subscribe reactively to invite state for any resource.