Host responsibilities
Authentication, authorization, delivery, billing, and migration remain in your app.
Trusted identity
Authenticate each public host wrapper. Never trust a user ID or email submitted as a client argument. Check that the invitation recipient email is verified by your authentication provider.
Component functions become internal references in the host. The host controls which operations clients can call. Keep direct addMember provisioning behind trusted internal functions.
Content access
Check current membership before each protected table, file, or media operation. An active-team preference does not grant access.
Team membership alone must not broaden access to personal content. Keep creator identity separate from workspace ownership when your product needs both.
Billing and capacity
Your app controls subscriptions and seat entitlements. Read the current entitlement in the mutation that grants membership. Pass seatLimit on each acceptInvite and addMember call.
Teams does not set prices, cancel subscriptions, or remove host content. Create durable host cleanup jobs when deleting workspaces.
Errors and retries
| Error | Response |
|---|---|
Not authorized. | Authenticate the correct actor or obtain the required membership. |
Team not found. | Resolve another workspace. The team is missing or deleted. |
Membership count is not ready. | Have the owner run prepareMembershipCount and wait for ready. |
Could not allocate a unique team slug. | Retry workspace creation. |
Team seat limit reached. | Change capacity before retrying the invitation. |
Membership no longer exists. | Obtain a new invitation. An accepted token cannot restore access. |
INVITATION_EXPIRED or INVITATION_REVOKED | Obtain a new invitation. |
INVITATION_AUDIENCE_MISMATCH | Use the verified intended recipient. |
Convex handles transaction conflicts. Do not catch a membership-grant error and return success from a host acceptance mutation.
Prepare counts for older teams
New workspaces start with an exact membership count. Older component records can lack this field. Before granting a new membership to an older team, run this method from an authenticated owner mutation:
await teams.prepareMembershipCount(ctx, userId, teamSlug);The method returns counting while background batches of at most 100 memberships run. Call it again to check for ready. Repeated calls are safe. New grants and invitation acceptance fail until the count is ready. Acceptance failure leaves the invitation usable for a later retry.
Existing access, removal, leave, and deletion remain available. A removal or leave during counting restarts the scan, so sustained removals can delay completion. Ownership transfer does not change the total. Completed count jobs cannot overwrite later grants.
Existing data
Import existing teams
Use trusted internal migration functions. These import methods do not authenticate a caller. Before import, freeze membership writes and validate the source snapshot. Confirm exactly one owner and unique memberships. Classify personal workspaces explicitly. Review legacy payment states separately; imported teams start as active.
const result = await teams.importTeam(ctx, {
teamPublicId: source.teamPublicId,
teamName: source.teamName,
teamSlug: source.teamSlug,
ownerUserId: source.ownerUserId,
personal: false,
expectedMemberCount: source.memberCount, // Includes the owner.
});Store result.teamId in a host mapping from the original host ID. Do not use a component ID as a host Id<"teams">. If the result is open, call importMembers with { teamPublicId, members } in batches of 1 to 100. Each member has a userId and owner, admin, or member role. The owner is already present and can be included in a batch. Then call finishImport(ctx, teamPublicId).
Repeating an open batch preserves identical memberships. Conflicting roles or owners fail the whole batch. finishImport requires the stored count to equal the expected snapshot count. That count is an import bound, not the host's paid seat entitlement. Compare the full imported membership list with the source before cutover.
If importTeam returns complete, skip the batches. finishImport is repeatable. Closed imports reject membership batches. Import receipts survive deletion and prevent replay from recreating the team. Do not remove receipts during cleanup.
Imported teams are active immediately. The host must delay routing consumers to them until verification completes. Keep source membership writes and destination membership writes paused during the import. This API does not enforce a migration lock or reconcile concurrent source changes.
The API preserves names, slugs, public IDs, and membership roles. Public IDs, names, and user IDs must be trimmed, nonempty strings of at most 256 characters. Slugs allow lowercase letters, digits, and single separating hyphens, up to 128 characters for legacy compatibility. New generated slugs remain limited to 60. Expected counts must be positive safe integers.
Imports create new timestamps. They do not import preferences, invitations, product permissions, content, or billing data. Keep source records and original timestamps for audit and recovery. Recovery after new destination writes requires reconciliation; switching configuration alone is insufficient.
The legacy teamInvites table and pending_payment schema value remain for migration inspection. Current invitation operations do not use the legacy table. Old tokens do not work with the new lifecycle.
Before upgrading an existing deployment, review its data. Define a migration or revoke and reissue old invitations. A complete consumer migration rehearsal remains a release gate.