# 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)