Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Invitations

Invite → accept → expire flow — issue a single-use, expiring invitation to a resource and consume it, as a Bijection component

Bijection source
git clone https://github.com/bijectionhq/invitations.git
cd invitations
git checkout c9c8c0921d2d0f69f4876f470d2da7418abeb2fa

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

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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 — 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<TRole, TPayload> 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

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

MethodKindResult
issue(ctx, resourceRef, opts?)mutation{ token, expiresAt } (opts: { role?; inviterRef?; inviteeRef?; payload?; ttlMs?; expiresAt? })
accept(ctx, token, acceptedBy)mutationInvitationGrant ({ resourceRef; role?; payload? })
revoke(ctx, token)mutationnull
peek(ctx, token)mutationInvitationView | null (read-time TTL enforcement)
getByToken(ctx, token)queryInvitationView | null (pure reactive read)
listPending(ctx, resourceRef, paginationOpts)queryPaginationResult<InvitationView>
listByResourceState(ctx, resourceRef, state, paginationOpts)queryPaginationResult<InvitationView>
prune(ctx, opts?)mutationnumber (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/payload are 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.

Keywords