Teams
Bijection workspaces with membership, ownership, preferences, and Bijection-invite integration.
git clone https://github.com/bijectionhq/teams.git
cd teams
git checkout 0c342e3016c3ae44e4a5d95622a63a24062e40e2
cd packages/convex-teamsBijection repository access is required. Request access.
Description
Bijection workspaces with membership, ownership, preferences, and Bijection-invite integration.
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/teams. Based on clipinfit/convex-teams. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Convex workspaces with membership, ownership, preferences, and convex-invite integration.
Explore the demo ↗Upstream overview
From the upstream package documentation.
Teams adds shared and personal workspaces to Convex applications. It manages team identity, owner/admin/member roles, invitations, seat limits, ownership transfer, and workspace preferences. A TeamsClient connects host functions to the component. The host supplies authenticated user identity and controls billing, message delivery, and access to application content.
Use cases
From the upstream package documentation.
- Add shared workspaces where users can invite teammates and manage roles.
- Enforce seat limits when users join, including simultaneous membership requests.
- Support personal workspaces and saved workspace preferences.
- Transfer shared workspace ownership before the current owner leaves.
- Import existing teams with stable public IDs and batched memberships.
How it works
From the upstream package documentation.
Mount @clipin/convex-teams in convex.config.ts, then create a TeamsClient with components.teams. The host must authenticate each caller before passing their identity to the client. Host wrappers control which operations users can call.
Teams stores membership counts and updates them in the same transaction as membership changes. Pass the current seatLimit when accepting an invitation or adding a member. Concurrent grants cannot exceed that limit. Pending invitations do not reserve seats.
The component mounts convex-invite as a child to manage invitation tokens, expiry, resend, revocation, and acceptance. The host handles delivery. The delivery example issues an invitation through an internal mutation and sends its token from action memory. Raw tokens must not be logged or stored in scheduled arguments.
Team, member, and pending invitation lists use pagination. Deleting a team denies access immediately, then removes memberships and repairs preferences in background batches. The host handles cleanup of its own content and subscriptions.
See https://convex-teams.vercel.app/docs/getting-started for installation and integration instructions.
Upstream package FAQ
What Convex version does @clipin/convex-teams require?
@clipin/convex-teams version 1.0.0 requires Convex >=1.43.0 and <2.0.0. The host mounts only the teams component, which internally mounts convex-invite@0.1.1 as a child component. No separate installation of convex-invite is needed.
Does @clipin/convex-teams handle authentication or email verification?
@clipin/convex-teams does not handle authentication, verified email checks, billing, or content permissions. The host application is responsible for deriving the actor from authentication and verifying the invitation recipient has a confirmed email before calling component methods. Component functions become internal references in the host, and host wrappers determine which operations clients can invoke.
How does @clipin/convex-teams prevent multiple concurrent grants from exceeding the seat limit?
@clipin/convex-teams uses a stored membership count on the team record rather than scanning all members to check capacity. Concurrent grant attempts contend on the same team record inside a Convex transaction, so only one can succeed when the final seat is available. Creation, grants, removal, and leave all update the count in the same transaction.
Can I migrate existing team data into @clipin/convex-teams?
@clipin/convex-teams provides trusted import methods: importTeam preserves an existing public team ID and creates its owner, importMembers accepts batches of 1 to 100 memberships, and finishImport verifies ownership and expected count before closing the import. These methods are intended for internal migration jobs, not public user endpoints. Imported teams are active immediately, and the host is responsible for enforcing any migration freeze on consumer access.
What happens to preferences and memberships when a team is deleted in @clipin/convex-teams?
Calling deleteTeam immediately marks the team deleted, which denies all access and invalidates invitation grants. Background cleanup removes memberships and repairs affected workspace preferences in batches of 50, giving each user a fallback workspace they can access or no workspace. A null redirect from preference lookup can be temporary while cleanup continues. The host must run its own content and subscription cleanup using the immutable teamPublicId as a reference, since @clipin/convex-teams does not delete host content or cancel subscriptions.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Teams with Bijection.
Read: https://bijection.com/components/teams/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from clipinfit/convex-teams, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@clipin/convex-teams
A Convex component for workspace identity, membership, ownership, and preferences. Public API names use team for a workspace.
Version 1.0.0 provides team identity, fixed owner/admin/member roles, invitations, and stable-ID imports. It uses convex-invite@0.1.1 for the invitation lifecycle.
npm install @clipin/convex-teams@1.0.0
Use Convex >=1.43.0 <2.0.0. Mount the component and create a trusted host client as shown in the getting started guide.
See the changelog and release procedure for versioning and release requirements.
Workspace layout
packages/convex-teams: publishable component and its runtime tests.packages/example-backend: host integration and concurrency proofs.apps/web: Next.js website and searchable Fumadocs documentation.docs: implementation plans and migration records.
Bun manages dependencies. Turborepo runs build, type checks, and tests across packages. Run bun run dev:web for the website or bun run dev:example for the local backend.
See website deployment for Vercel settings and deployment commands.
Run the development example
Use Bun 1.4.0 and Node.js 22 or later.
bun install --frozen-lockfile
bun run build
cd packages/example-backend
bun run dev
The example uses an anonymous local Convex deployment. It does not connect to Feedtwin or production data. From another terminal, run these commands at the repository root:
bun run codegen
bun run typecheck
bun run test
bun run lint
bun run build
bun run pack:check
Run the backend concurrency proof from packages/example-backend:
bunx convex run proof:run
bunx convex run proof:concurrentBootstrap
bunx convex run proof:concurrentDuplicateGrant
bunx convex run proof:largeSeatLimit
bunx convex run proof:concurrentSlugs
bunx convex run migrationProof:run
bunx convex run migrationProof:runLifecycle
bunx convex run migrationProof:runRecovery
The proof creates a development fixture. One direct grant and one invitation acceptance compete for the final seat. The expected result is one successful grant, one rejected grant, and two members.
Component ownership
| Component | Responsibility |
|---|---|
convex-teams | Team identity, membership, roles, ownership transfer, preferences, invitation scope, and membership grants |
convex-invite | Tokens, expiry, resend, revocation, acceptance, and delivery metadata |
| Host application | Authentication, verified email, current seat policy, message delivery, billing, and content permissions |
Teams mounts convex-invite as its child. The host mounts only teams. See the example configuration.
Teams uses the published convex-invite@0.1.1 dependency. This version fixes invitation pagination inside component mounts. No local dependency patch is required.
Host integration
Create a TeamsClient with your host's generated components.teams reference. The client methods accept contexts from host Convex functions. The host example derives the actor from authentication and checks that the invitation recipient has a verified email.
Never expose a submitted actor ID or email as trusted identity. Component functions become internal references in the host. Host wrappers determine which operations clients can call.
The host must check membership before each protected content operation. An active-team preference does not grant access to host tables, files, or media URLs.
Slug allocation
Creation tries at most five slug candidates. The first shared workspace can use the name alone. Collisions use a random suffix instead of a sequential number. Personal workspaces always use a random suffix. Generated slugs stay within 60 characters. If all attempts collide, creation fails atomically and the host can retry.
Roles
| Operation | Owner | Admin | Member |
|---|---|---|---|
| Read team and member information | Yes | Yes | Yes |
| Change profile | Yes | Yes | No |
| Invite and manage non-owner members | Yes | Yes | No |
| List invitation recipient information | Yes | Yes | No |
| Transfer ownership | Yes | No | No |
| Delete team | Yes | No | No |
| Leave team | Transfer ownership first | Yes | Yes |
Each team has one owner. transferOwnership changes the owner record and both membership roles in one transaction. The former owner becomes an admin. Invitations and direct grants preserve an existing role. Use updateMemberRole for an explicit role change.
Personal team creation is an explicit host operation through ensurePersonalTeam. A personal team has a separate personal-owner field. Default-team selection does not define personal ownership. Personal ownership cannot be transferred. Invitation acceptance does not create a personal team.
Seats and invitations
Pending invitations do not reserve seats. On each acceptInvite or trusted addMember call, pass the current seatLimit from host configuration. Read that configuration in the same host mutation. Omit the limit only for an unlimited policy. A limit must be a nonnegative safe integer. The owner consumes a seat.
Seat checks read a stored membership count instead of scanning the team's members. Creation, grants, removal, and leave update that count in the same transaction. Duplicate grants and role changes do not increase it. Concurrent grants still contend on the team record so they cannot oversubscribe the final seat.
For team records created before count tracking, the owner must call prepareMembershipCount(ctx, userId, teamSlug) through an authenticated host mutation. It returns counting until background batches of at most 100 memberships finish, then returns ready. New grants, including invitation acceptance, fail until the count is ready. Existing access, removal, leave, and deletion remain available. A removal during counting restarts the scan. Repeated preparation and count-job delivery are safe.
A capacity failure rolls back both child invitation acceptance and the new membership. Existing members retain access after a limit reduction. Repeated acceptance preserves the member's current role. A previously accepted invitation cannot restore a removed member.
createInvite now creates a new invitation. A duplicate pending invitation produces INVITATION_ALREADY_PENDING. Use resendInvite to rotate its token. Use the returned invitation ID after a resend.
listTeams, listMembers, and listPendingInvites require paginationOpts and return page, isDone, and continueCursor. Request 1 to 100 rows per page. Team pages use membership order and can be empty during deletion cleanup. Continue until isDone, even after an empty page. The host can sort the returned teams for display. Member and invitation lists are separate. Use the convex-helpers pagination hook for a reactive host UI.
Delivery
The delivery example authenticates the actor in a host action. It calls an internal host mutation to issue the invitation, then sends the token from action memory to the provider.
Never store or log the raw token. Never include it in scheduled arguments. To schedule delivery, schedule only the recipient and team information, then issue the token inside the action. Record only safe delivery metadata. The example catches provider errors and returns a fixed state without provider error text.
A provider failure leaves the invitation available for explicit resend. If the provider succeeds but metadata recording fails, delivery is uncertain. Reconcile that state before sending another message. No exactly-once delivery guarantee is made.
Deletion and retention
deleteTeam requires the owner. It immediately marks the team deleted, which denies access and invalidates its invitation grants. Cleanup removes memberships and repairs affected preferences in batches of 50. Each user gets a fallback they can access, or no workspace. Fallback repair checks saved preferences directly, then scans 25 memberships per transaction. It schedules another page if necessary. A null redirect can therefore be temporary while cleanup continues. A later explicit selection takes precedence over the background repair. The final cleanup removes the team record. Cleanup retries are safe.
The host must keep any content and subscription cleanup job in its own tables. Create that job in the same host mutation that requests deletion. Use the immutable teamPublicId as its reference. Teams does not cancel subscriptions or delete host content.
Call pruneInvitations from trusted host maintenance. The invite component expires pending records and removes terminal records after its 90-day retention period. Each call is bounded. The host must run enough batches for its invitation volume.
The old teamInvites table and pending_payment schema value remain only for migration inspection. No invitation operation uses the old table. Old tokens cannot be accepted by the new lifecycle. Do not deploy over existing invitation data until migration or explicit revocation with reissue has been agreed. The payment activation API, generation permission, and fixed profile throttle have been removed.
Errors and retries
Not authorized.: authenticate the correct actor or obtain the required membership. Do not retry unchanged.Team not found.: the team is missing or deleted. Resolve another workspace.Team seat limit reached.: update capacity or remove another member before retrying the same invitation.Membership no longer exists.: obtain a new invitation. An accepted token cannot restore removed access.INVITATION_EXPIRED,INVITATION_REVOKED, orINVITATION_AUDIENCE_MISMATCH: use the correct recipient or obtain a new invitation.
Convex handles transaction conflicts. Do not catch a membership-grant error and return success from a host acceptance mutation.
See the completion PRD for remaining work and the Feedtwin migration map for consumer constraints.
Trusted imports
importTeam preserves an existing public team ID and creates its owner. importMembers accepts batches of 1 to 100 memberships. finishImport checks ownership and the expected count before closing the import. These methods are for trusted internal migration jobs, not public user endpoints.
Keep membership writes paused and delay consumer cutover until all batches and access comparisons pass. Imported teams are active immediately; the component does not enforce the host migration freeze. Store the old-host-ID to component-ID mapping in the host. Keep project-only memberships, billing references, and content ownership in the host.
Open batches are repeatable with identical roles. Conflicts fail the whole batch. Once complete, skip batch replay. Durable receipts prevent a completed import from restoring removed members or recreating a deleted team. Retain the source snapshot and receipts. Imports do not migrate preferences, invitations, or source timestamps. See the migration contract.
getTeamState(ctx, teamPublicId) is a trusted host lookup of current workspace metadata. Missing or deleted teams return null. A returned record is not an access grant. The host must apply its membership or resource-specific permission rules.