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-invitations@vllnt/convex-invitations manages the lifecycle of an invitation independently of the resource being shared. Your app supplies the resource reference and typed role or payload, then decides how to deliver and apply the invitation.
issue returns a token and expiry. accept consumes a valid invitation and returns the grant for the host to apply. revoke cancels pending invitations; accepted, revoked, and expired states are terminal. Queries list invitations by resource and state.
Your backend authenticates the recipient, authorizes the action, delivers the link, and applies the returned grant. The component does not send email or create membership automatically. Coordinate acceptance and the host-side grant so failures cannot leave the user without the intended access.
Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt).
@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.