# Invitations Invite → accept → expire flow — issue a single-use, expiring invitation to a resource and consume it, as a Bijection component - Package: @bijectionhq/invitations - Category: Collaboration - Status: Preview - License: MIT - Bijection source (repository access required): https://github.com/bijectionhq/invitations/tree/c9c8c0921d2d0f69f4876f470d2da7418abeb2fa - Upstream: https://github.com/vllnt/convex-invitations/tree/ebbf6c481acc35f08cacb9207c0daad986426054 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/invitations ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/invitations.git cd invitations git checkout c9c8c0921d2d0f69f4876f470d2da7418abeb2fa ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @vllnt/convex-invitations; check the Bijection adaptation separately. @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). ### Use cases - SaaS onboarding: invite a colleague to a workspace with a proposed role. - Education: offer access to a course or learning group. - Games and communities: invite someone to a team or guild. - Collaboration: share access to a project or document through an expiring link. ### How it works 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. ### Upstream README # @vllnt/convex-invitations The invite → accept → expire flow, as a Convex component. ```ts const invites = new Invitations(components.invitations); const { token } = await invites.issue(ctx, orgId, { role: "member" }); // host delivers token const grant = await invites.accept(ctx, token, userId); // single-use; host applies grant ``` A host mutation issues a single-use, expiring invitation to an opaque resource and gets back a token to deliver out of band; the invitee redeems it with `accept`, which consumes the invite and returns the grant the host applies. Domain-neutral: a game guild invite, a SaaS workspace invite, a publication contributor invite. ## Features - **Issue-and-deliver** — `issue` inserts a `pending` invite, returns a single-use `token` + `expiresAt`; the host delivers it (never sends email). - **Single-use accept** — `accept` consumes the invite and returns the grant (`resourceRef` + opaque `role`/`payload`) the host applies. - **Terminal states are final** — `accepted`/`revoked`/`expired` are terminal; a replayed link can never double-grant. - **TTL + expiry** — every invite carries an `expiresAt`; accepting past-TTL is rejected; a daily cron and `peek` sweep stale invites. - **Revoke** — `revoke` cancels a still-`pending` invite; a revoke racing an accept yields exactly one winner. - **Poll or subscribe** — `getByToken`, `listPending`, and `listByResourceState` page invites reactively in Convex. - **Typed, opaque host data** — `Invitations` with optional `roleValidator`/`payloadValidator` at the boundary; server-sourced time. - **Bounded prune + cron, mount-safe** — a daily cron expires + prunes in idempotent batches; correct under multiple named mounts. ## Installation ```bash pnpm add @vllnt/convex-invitations ``` Peer dependency: `convex@^1.41.0`. ## Usage ```ts // convex/convex.config.ts import { defineApp } from "convex/server"; import invitations from "@vllnt/convex-invitations/convex.config"; const app = defineApp(); app.use(invitations); export default app; ``` ```ts // convex/invites.ts — host owns auth, the resource meaning, and delivery. import { components } from "./_generated/api"; import { mutation } from "./_generated/server"; import { v } from "convex/values"; import { Invitations } from "@vllnt/convex-invitations"; const invites = new Invitations<"admin" | "member", never>(components.invitations, { ttlMs: 1000 * 60 * 60 * 24 * 3, // 3-day links roleValidator: v.union(v.literal("admin"), v.literal("member")).parse, }); // Issue: record the invite, return the token to deliver. export const invite = mutation({ args: { orgId: v.string(), role: v.union(v.literal("admin"), v.literal("member")) }, handler: async (ctx, { orgId, role }) => { const { token } = await invites.issue(ctx, orgId, { role }); // host gates the caller first return { token }; // host emails `https://app/join?token=${token}` }, }); // Accept: consume the invite, apply the grant (write your own membership). export const join = mutation({ args: { token: v.string() }, handler: async (ctx, { token }) => { const me = await currentUser(ctx); // host resolves identity const grant = await invites.accept(ctx, token, me); await ctx.db.insert("memberships", { orgId: grant.resourceRef, userId: me, role: grant.role }); }, }); ``` Client options: `new Invitations(component, { ttlMs?, generateToken?, roleValidator?, payloadValidator? })`. ## API Reference | Method | Kind | Result | |--------|------|--------| | `issue(ctx, resourceRef, opts?)` | mutation | `{ token, expiresAt }` (`opts`: `{ role?; inviterRef?; inviteeRef?; payload?; ttlMs?; expiresAt? }`) | | `accept(ctx, token, acceptedBy)` | mutation | `InvitationGrant` (`{ resourceRef; role?; payload? }`) | | `revoke(ctx, token)` | mutation | `null` | | `peek(ctx, token)` | mutation | `InvitationView \| null` (read-time TTL enforcement) | | `getByToken(ctx, token)` | query | `InvitationView \| null` (pure reactive read) | | `listPending(ctx, resourceRef, paginationOpts)` | query | `PaginationResult` | | `listByResourceState(ctx, resourceRef, state, paginationOpts)` | query | `PaginationResult` | | `prune(ctx, opts?)` | mutation | `number` (invites touched in the first bounded pass) | Full reference: [docs/API.md](docs/API.md). ## React Backend-only — no `./react` entry. An invite-management surface is an ordinary reactive `useQuery` over the host's own re-exported `getByToken` / `listPending` refs; the token is delivered by the host, never revealed in a client component. ## Security - **Auth-agnostic** — the host authenticates the caller, decides who may issue/accept/revoke, and owns token delivery; tables are sandboxed. - **Single-use + server-sourced time** — terminal states are final (no double-grant); TTL is enforced on read; timestamps come from `Date.now()`, never the caller. - **Opaque host data** — `resourceRef`/`role`/`payload` are opaque, narrowed only by the optional host validators at the client boundary. See [docs/API.md](docs/API.md). ## Testing ```bash pnpm test # single run pnpm test:coverage # enforced 100% on covered files ``` Tests run against the real component runtime via `convex-test` (`@edge-runtime/vm`), not mocks. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). ## Author Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt) Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com) If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt). ## License MIT — see [LICENSE](LICENSE).