Invitations
Invite → accept → expire flow — issue a single-use, expiring invitation to a resource and consume it, as a Bijection component
git clone https://github.com/bijectionhq/invitations.git
cd invitations
git checkout c9c8c0921d2d0f69f4876f470d2da7418abeb2faBijection repository access is required. Request access.
Description
Invite → accept → expire flow — issue a single-use, expiring invitation to a resource and consume it, as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/invitations. Based on vllnt/convex-invitations. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Invite → accept → expire flow — issue a single-use, expiring invitation to a resource and consume it, as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@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 at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- 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
From the upstream package documentation.
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 package FAQ
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.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Invitations with Bijection.
Read: https://bijection.com/components/invitations/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-invitations, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-invitations
The invite → accept → expire flow, as a Convex component.
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 —
issueinserts apendinginvite, returns a single-usetoken+expiresAt; the host delivers it (never sends email). - Single-use accept —
acceptconsumes the invite and returns the grant (resourceRef+ opaquerole/payload) the host applies. - Terminal states are final —
accepted/revoked/expiredare terminal; a replayed link can never double-grant. - TTL + expiry — every invite carries an
expiresAt; accepting past-TTL is rejected; a daily cron andpeeksweep stale invites. - Revoke —
revokecancels a still-pendinginvite; a revoke racing an accept yields exactly one winner. - Poll or subscribe —
getByToken,listPending, andlistByResourceStatepage invites reactively in Convex. - Typed, opaque host data —
Invitations<TRole, TPayload>with optionalroleValidator/payloadValidatorat the boundary; server-sourced time. - Bounded prune + cron, mount-safe — a daily cron expires + prunes in idempotent batches; correct under multiple named mounts.
Installation
pnpm add @vllnt/convex-invitations
Peer dependency: convex@^1.41.0.
Usage
// 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;
// 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<InvitationView> |
listByResourceState(ctx, resourceRef, state, paginationOpts) | query | PaginationResult<InvitationView> |
prune(ctx, opts?) | mutation | number (invites touched in the first bounded pass) |
Full reference: 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/payloadare opaque, narrowed only by the optional host validators at the client boundary.
See docs/API.md.
Testing
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.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.