Invitations

Single-use invitation tokens with TTL enforcement and typed grants for Convex apps — issue, accept, revoke, and expire invite flows in sandboxed tables.

Installation

npm install @vllnt/convex-invitations

About Invitations

Invitations 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.

Benefits

Use cases

how to implement single-use invite links in Convex

@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.

Convex multi-tenant workspace invitation system

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.

TTL expiry enforcement for invitation tokens Convex

@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.

revoke pending invite Convex before it is accepted

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.

Frequently asked questions

Does @vllnt/convex-invitations handle email sending or token delivery?

@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.

Can the same invite token be accepted more than once?

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.

How do I add type safety for roles and payloads in convex-invitations?

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.

Does convex-invitations work with multiple named component mounts?

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.

How do I list or reactively subscribe to pending invites for a resource?

@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.

Links