# Convex Invite-links Membership and invite management for Convex — shareable invite links with expiration, email locking, and group access control. This is an upstream package. It has not been adapted or qualified for Bijection. - Package: convex-invite-links - Source: https://github.com/TimpiaAI/convex-invite-links - npm: https://www.npmjs.com/package/convex-invite-links - Directory: https://bijection.com/components/convex-invite-links Read the upstream README for setup, license, and compatibility before using it. ## Upstream package documentation Recorded 2026-10-23. This describes convex-invite-links; check the Bijection adaptation separately. A Convex component that provides membership tracking and invite-based access control through shareable links. It handles group-level and resource-level permissions, invite lifecycle management with expiration and email locking, and includes HTTP endpoints for social preview generation. The component offers both direct membership management and invite-flow onboarding with comprehensive validation and cleanup utilities. ### Use cases • **Team onboarding workflows** where you need invite links that grant group membership with role-based access and email validation • **Document or resource sharing** that requires time-limited access control with revocation capabilities • **Multi-tenant applications** managing user permissions across different groups or organizations with invite tracking • **Collaborative platforms** needing automated membership management with social preview links for better user experience • **Applications with approval flows** where administrators create invites for specific users with predefined access levels ### How it works The component registers with your Convex app and exposes membership APIs like `isMember()`, `hasAccess()`, and `getAllGroups()` for querying permissions, plus mutation functions like `addMember()` and `grantAccess()` for direct access management. Invite creation happens through `makeInvite()` which generates tokens with optional email locks, expiration dates, and metadata, while `claimInvite()` validates and applies the invite to grant appropriate access. HTTP integration is handled by `handleInviteWebhook()` which processes GET requests to invite URLs and returns HTML with Open Graph metadata for social previews. The component includes utility functions like `makeInviteUrl()` for generating shareable links and `inviteFromUrl()` for parsing tokens from requests. Invite management includes `revokeInvite()` for immediate invalidation, `listInvites()` with filtering options, and `deleteOldInvites()` for scheduled cleanup through Convex cron jobs. The system tracks invite status (pending, claimed, expired, revoked) and provides detailed validation results during the claiming process. ### Upstream README # convex-invite-links A [Convex](https://convex.dev) component for **membership and invite management** — create shareable invite links with expiration, email locking, group/resource access control, and full lifecycle tracking. Built for apps that need invite-based onboarding, team management, resource sharing, and access control. > **Convex Components Challenge** — Invite Links: Membership Tracking, Invite Creation, Claiming, and HTTP Integration ## Features - **Membership Tracking** — `isMember()`, `hasAccess()`, `getAllGroups()`, `getAllMembers()`, `addMember()`, `grantAccess()` - **Generic Access Control** — Group-level and resource-level access, configurable per invite - **Invite Creation** — `makeInvite()` with optional email/phone lock, expiration, destination URL, and metadata - **Configurable Defaults** — Set default expiration at component initialization - **Invite Revocation** — `revokeInvite()` to immediately invalidate by ID or token - **Invite Listing** — `listInvites()` with filters for group, resource, user, and status - **Invite Cleanup** — `deleteOldInvites()` for scheduled garbage collection - **Invite Claiming** — `claimInvite()` validates token, checks recipient lock, auto-grants access - **Pending Invite Check** — `hasInvite()` to check for unclaimed invites - **HTTP Integration** — `handleInviteWebhook` for direct invite URL visits with social previews - **URL Helpers** — `makeInviteUrl()` and `inviteFromUrl()` for link generation and parsing - **Social Previews** — `og:title`, `og:description` metadata on invite pages - **TypeScript** — Fully typed with exported interfaces for all result types ## Installation ```bash npm install convex-invite-links ``` ## Setup ### 1. Register the component ```ts // convex/convex.config.ts import { defineApp } from "convex/server"; import inviteLinks from "convex-invite-links/convex.config"; const app = defineApp(); app.use(inviteLinks); export default app; ``` ### 2. Initialize the client ```ts // convex/invites.ts import { InviteLinks } from "convex-invite-links"; import { components } from "./_generated/api.js"; const invites = new InviteLinks(components.inviteLinks, { defaultExpiryMs: 7 * 24 * 60 * 60 * 1000, // 7 days baseUrl: "https://myapp.com/invite", }); ``` ## Usage ### Create an invite ```ts import { mutation } from "./_generated/server.js"; import { v } from "convex/values"; export const createInvite = mutation({ args: { email: v.optional(v.string()), groupId: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); const result = await invites.makeInvite(ctx, { email: args.email, groupId: args.groupId, createdBy: userId, destinationUrl: "https://myapp.com/dashboard", metadata: { role: "member" }, ogTitle: "Join My Team", ogDescription: "You've been invited to collaborate!", }); // Generate a shareable URL const url = invites.makeInviteUrl(result.token); return { ...result, url }; }, }); ``` ### Claim an invite ```ts export const acceptInvite = mutation({ args: { token: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); const userEmail = await getUserEmail(ctx); const result = await invites.claimInvite(ctx, { token: args.token, userId, email: userEmail, }); if (!result.ok) { throw new Error(`Invite invalid: ${result.reason}`); // reason: "invalid" | "expired" | "revoked" | "already_claimed" // | "email_mismatch" | "phone_mismatch" } return { destinationUrl: result.destinationUrl, groupId: result.groupId, }; }, }); ``` ### Check membership ```ts export const checkAccess = query({ args: { groupId: v.string() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); const isMember = await invites.isMember(ctx, { userId, groupId: args.groupId, }); return { isMember }; }, }); ``` ### Direct membership management ```ts // Add a user to a group directly (without invite) await invites.addMember(ctx, { userId: targetUserId, groupId: "team_123", addedBy: adminUserId, }); // Grant resource access directly await invites.grantAccess(ctx, { userId: targetUserId, resourceId: "doc_456", grantedBy: adminUserId, }); // Remove membership or access await invites.removeMember(ctx, { userId, groupId: "team_123" }); await invites.revokeAccess(ctx, { userId, resourceId: "doc_456" }); ``` ### Revoke and manage invites ```ts // Revoke by ID await invites.revokeInvite(ctx, { inviteId: "invite_id_here" }); // Revoke by token await invites.revokeInviteByToken(ctx, { token: "inv_abc123..." }); // List pending invites for a group const pending = await invites.listInvites(ctx, { groupId: "team_123", }); // List all invites including claimed/expired const all = await invites.listInvites(ctx, { groupId: "team_123", includeClaimed: true, includeRevoked: true, includeExpired: true, }); ``` ### Schedule automatic cleanup ```ts // In a cron job (convex/crons.ts) import { cronJobs } from "convex/server"; import { internal } from "./_generated/api.js"; const crons = cronJobs(); crons.daily("cleanup invites", { hourUTC: 3, minuteUTC: 0 }, internal.invites.cleanupOld); export default crons; // In your invites file export const cleanupOld = internalMutation({ handler: async (ctx) => { const deleted = await invites.deleteOldInvites(ctx, { olderThanMs: 30 * 24 * 60 * 60 * 1000, }); console.log(`Cleaned up ${deleted} old invites`); }, }); ``` ### HTTP integration with social previews ```ts import { handleInviteWebhook } from "convex-invite-links"; import { httpRouter } from "convex/server"; const http = httpRouter(); const inviteHandler = handleInviteWebhook(components.inviteLinks, { appName: "My App", redirectUrl: "https://myapp.com/accept-invite", }); http.route({ path: "/invite", method: "GET", handler: httpAction(async (ctx, request) => { return await inviteHandler(ctx, request); }), }); export default http; ``` ### Parse invite tokens from URLs ```ts // In an HTTP action or server function const token = invites.inviteFromUrl(request); if (token) { const invite = await invites.getInviteByToken(ctx, { token }); if (invite?.isValid) { // Show invite acceptance page } } ``` ## API Reference ### Membership | Method | Context | Description | |--------|---------|-------------| | `isMember(ctx, args)` | query | Check if user is in a group | | `hasAccess(ctx, args)` | query | Check if user has resource access | | `getAllGroups(ctx, args)` | query | List groups for a user | | `getAllMembers(ctx, args)` | query | List members of a group | | `addMember(ctx, args)` | mutation | Add user to group | | `removeMember(ctx, args)` | mutation | Remove user from group | | `grantAccess(ctx, args)` | mutation | Grant resource access | | `revokeAccess(ctx, args)` | mutation | Revoke resource access | ### Invite Management | Method | Context | Description | |--------|---------|-------------| | `makeInvite(ctx, args)` | mutation | Create invite with optional locks | | `revokeInvite(ctx, args)` | mutation | Revoke invite by ID | | `revokeInviteByToken(ctx, args)` | mutation | Revoke invite by token | | `listInvites(ctx, args?)` | query | List invites with filters | | `deleteOldInvites(ctx, args?)` | mutation | Cleanup old invites | ### Invite Claiming | Method | Context | Description | |--------|---------|-------------| | `claimInvite(ctx, args)` | mutation | Validate and apply invite | | `hasInvite(ctx, args)` | query | Check for pending invites | | `getInviteByToken(ctx, args)` | query | Get invite details by token | ### HTTP Integration | Function | Description | |----------|-------------| | `handleInviteWebhook(component, options)` | HTTP handler with social preview metadata | | `invites.makeInviteUrl(token)` | Generate shareable invite URL | | `invites.inviteFromUrl(request)` | Parse token from URL/Request | ### Group Management | Method | Context | Description | |--------|---------|-------------| | `upsertGroup(ctx, args)` | mutation | Create or update group | | `getGroup(ctx, args)` | query | Get group details | ## Live Demo Check out the [live demo](https://invite-links-demo.vercel.app) for a working example. ## Author Built and maintained by [TimpiaAI](https://github.com/TimpiaAI). ## License [MIT](https://github.com/TimpiaAI/convex-invite-links/blob/main/LICENSE)