{"_id":"@clipin/convex-teams","_rev":"2-f9b9424786539d266f3f11625f05636c","name":"@clipin/convex-teams","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@clipin/convex-teams","version":"1.0.0","keywords":["convex","convex-component","teams","rbac","organizations","members","invites","permissions","multi-tenant"],"author":{"name":"Denis Ciccale"},"license":"Apache-2.0","_id":"@clipin/convex-teams@1.0.0","maintainers":[{"name":"denis","email":"dciccale@gmail.com"}],"homepage":"https://convex-teams.vercel.app","bugs":{"url":"https://github.com/clipinfit/convex-teams/issues"},"dist":{"shasum":"35a832215bb49bc8be33166dd4f22b4fb59dc8eb","tarball":"https://registry.npmjs.org/@clipin/convex-teams/-/convex-teams-1.0.0.tgz","fileCount":85,"integrity":"sha512-HcesqbNr55olQfdYe+/kiIDMMmJ5ETw2jrPhRbWoCIwnE71b7sLcj/Oq3BNZ6O3B9W0O17pBMNPx1WCalZq9ww==","signatures":[{"sig":"MEYCIQD+HPSr6tL5WyIWoEHRgFyXdxQdCwRre5cVTN+Wzw1tGAIhAKqNxlPW4hCxyKcJ12oV9MC3gqEjShiHL3u7IcC9d0ON","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":354363},"main":"./dist/client/index.js","type":"module","types":"./dist/client/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./types":{"types":"./dist/client/types.d.ts","default":"./dist/client/types.js"},"./package.json":"./package.json","./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"}},"gitHead":"113267c9ba29d98ffa5420a0bf6b8c40eb6223b2","scripts":{"dev":"tsc -p tsconfig.build.json --watch","lint":"biome check .","test":"vitest run","build":"tsc -p tsconfig.build.json","format":"biome format --write .","codegen":"cd ../example-backend && convex codegen --component-dir ../convex-teams/src/component","prepack":"npm run build","lint:fix":"biome check --write .","prebuild":"node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"","typecheck":"tsc --noEmit","pack:check":"node ../../scripts/check-package.mjs","test:watch":"vitest","prepublishOnly":"node ../../scripts/check-release.mjs"},"_npmUser":{"name":"denis","email":"dciccale@gmail.com"},"repository":{"url":"git+https://github.com/clipinfit/convex-teams.git","type":"git","directory":"packages/convex-teams"},"_npmVersion":"11.19.0","description":"Convex workspaces with membership, ownership, preferences, and convex-invite integration.","directories":{},"_nodeVersion":"26.7.0","dependencies":{"convex-invite":"0.1.1","convex-helpers":"^0.1.124"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.0","vite":"8.2.1","convex":"1.43.0","vitest":"4.1.10","typescript":"7.0.2","@types/node":"^22.0.0","convex-test":"0.0.55","@biomejs/biome":"2.4.7","@edge-runtime/vm":"^5.0.0"},"peerDependencies":{"convex":">=1.43.0 <2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/convex-teams_1.0.0_1788803658153_0.2786015774733217","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@clipin/convex-teams","version":"1.0.1","description":"Convex workspaces with membership, ownership, preferences, and convex-invite integration.","license":"Apache-2.0","homepage":"https://convex-teams.vercel.app","bugs":{"url":"https://github.com/clipinfit/convex-teams/issues"},"type":"module","main":"./dist/client/index.js","types":"./dist/client/index.d.ts","exports":{"./package.json":"./package.json",".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"},"./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./types":{"types":"./dist/client/types.d.ts","default":"./dist/client/types.js"}},"publishConfig":{"access":"public"},"scripts":{"prebuild":"node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"","build":"tsc -p tsconfig.build.json","dev":"tsc -p tsconfig.build.json --watch","format":"biome format --write .","lint":"biome check .","lint:fix":"biome check --write .","prepack":"npm run build","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","codegen":"cd ../example-backend && convex codegen --component-dir ../convex-teams/src/component","pack:check":"node ../../scripts/check-package.mjs","prepublishOnly":"node ../../scripts/check-release.mjs"},"peerDependencies":{"convex":">=1.43.0 <2.0.0"},"devDependencies":{"@biomejs/biome":"2.4.7","@edge-runtime/vm":"^5.0.0","@types/node":"^22.0.0","convex":"1.43.0","convex-test":"0.0.55","tsx":"^4.19.0","typescript":"7.0.2","vite":"8.2.1","vitest":"4.1.10"},"repository":{"type":"git","url":"git+https://github.com/clipinfit/convex-teams.git","directory":"packages/convex-teams"},"keywords":["convex","convex-component","teams","rbac","organizations","members","invites","permissions","multi-tenant"],"dependencies":{"convex-helpers":"^0.1.124","convex-invite":"0.1.1"},"author":{"name":"Denis Ciccale"},"engines":{"node":">=20"},"gitHead":"77eaed196bd55e19d03a0f912c9efc8993141a53","_id":"@clipin/convex-teams@1.0.1","_nodeVersion":"26.7.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-L5vBi+12/0WHmqQeS16RHVWykQi9yacpvnFZRT+ljvNKwOq1DI1oMH6orXjQTKatLKWTcNiy3EzD5r2CFtziIw==","shasum":"74c648d3f7e9e7f871775a4ca4f959161a32fc46","tarball":"https://registry.npmjs.org/@clipin/convex-teams/-/convex-teams-1.0.1.tgz","fileCount":85,"unpackedSize":352150,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIE3pBcTryLGiEOVhJrRhLSTQjN4FQ42KNbV1kFAUu3TeAiEAikHDP3zdxE3Gx6cB8Kk27t+yPmiljujacop1dyDqY4s="}]},"_npmUser":{"name":"denis","email":"dciccale@gmail.com"},"directories":{},"maintainers":[{"name":"denis","email":"dciccale@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convex-teams_1.0.1_1789206223001_0.552097396628362"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-07T17:54:17.999Z","modified":"2026-09-12T09:43:43.301Z","1.0.0":"2026-09-07T17:54:18.280Z","1.0.1":"2026-09-12T09:43:43.144Z"},"bugs":{"url":"https://github.com/clipinfit/convex-teams/issues"},"author":{"name":"Denis Ciccale"},"license":"Apache-2.0","homepage":"https://convex-teams.vercel.app","keywords":["convex","convex-component","teams","rbac","organizations","members","invites","permissions","multi-tenant"],"repository":{"type":"git","url":"git+https://github.com/clipinfit/convex-teams.git","directory":"packages/convex-teams"},"description":"Convex workspaces with membership, ownership, preferences, and convex-invite integration.","maintainers":[{"name":"denis","email":"dciccale@gmail.com"}],"readme":"# @clipin/convex-teams\n\n[Website](https://convex-teams.vercel.app) · [Documentation](https://convex-teams.vercel.app/docs)\n\nA Convex component for adding teams and shared workspaces to your app. It manages team profiles, members, roles, invitations, and workspace selection. The API calls each workspace a `team`.\n\nUse it to let users create a team, invite collaborators, switch workspaces, and manage access to their team.\n\n## Features\n\n- Shared and personal teams with unique slugs and stable public IDs.\n- Owner, admin, and member roles with ownership transfer.\n- Invitations with expiry, resend, revocation, and verified recipient acceptance.\n- Seat limits enforced when a member joins, including concurrent requests.\n- Active and default team preferences for each user.\n- Paginated team, member, and pending invitation lists.\n- Team deletion with background membership cleanup and preference repair.\n\nYour app provides authentication, invitation delivery, billing, and permissions for its own content.\n\n## Quick start\n\nInstall the component in your Convex app. It requires Convex `>=1.43.0 <2.0.0`.\n\n```sh\nnpm install @clipin/convex-teams@1.0.1\n```\n\nRegister the component in `convex/convex.config.ts`:\n\n```ts\nimport { defineApp } from \"convex/server\";\nimport teams from \"@clipin/convex-teams/convex.config.js\";\n\nconst app = defineApp();\napp.use(teams);\nexport default app;\n```\n\nRun `npx convex dev` to generate the component API. Then create a client and expose authenticated functions in `convex/teams.ts`:\n\n```ts\nimport { TeamsClient } from \"@clipin/convex-teams\";\nimport { paginationOptsValidator } from \"convex/server\";\nimport { v } from \"convex/values\";\nimport { components } from \"./_generated/api.js\";\nimport { mutation, query } from \"./_generated/server.js\";\n\nconst teams = new TeamsClient(components.teams);\n\nexport const create = mutation({\n\targs: { name: v.string() },\n\thandler: async (ctx, args) => {\n\t\tconst identity = await ctx.auth.getUserIdentity();\n\t\tif (!identity) throw new Error(\"Not authorized.\");\n\t\treturn teams.createTeam(ctx, identity.subject, args.name);\n\t},\n});\n\nexport const list = query({\n\targs: { paginationOpts: paginationOptsValidator },\n\thandler: async (ctx, args) => {\n\t\tconst identity = await ctx.auth.getUserIdentity();\n\t\tif (!identity) throw new Error(\"Not authorized.\");\n\t\treturn teams.listTeams(ctx, identity.subject, args.paginationOpts);\n\t},\n});\n```\n\nCall `api.teams.create` with `{ name: \"Design studio\" }` from your app. The result includes `teamId`, `teamPublicId`, `teamSlug`, and `teamName`. The authenticated user becomes the owner.\n\nCall `api.teams.list` with `{ paginationOpts: { numItems: 25, cursor: null } }` to read that user's teams.\n\nThe example uses `identity.subject` as the user ID. Use the same authenticated user ID for all component calls. Derive identity in your backend; do not accept a caller-supplied user ID as proof of identity.\n\nSee the [API reference](https://convex-teams.vercel.app/docs/api) for all client methods.\n\n## Slug allocation\n\nA slug identifies a team in URLs. Shared teams first try a slug based on the team name. If that slug is taken, the component adds a random suffix. Personal teams always use a random suffix.\n\nGenerated slugs stay within 60 characters. Creation tries at most five candidates. If all candidates collide, creation fails without creating a team. Your app can retry.\n\n## Roles\n\n| Operation | Owner | Admin | Member |\n| --- | --- | --- | --- |\n| Read team and member information | Yes | Yes | Yes |\n| Change profile | Yes | Yes | No |\n| Invite and manage non-owner members | Yes | Yes | No |\n| List invitation recipient information | Yes | Yes | No |\n| Transfer ownership | Yes | No | No |\n| Delete team | Yes | No | No |\n| Leave team | Transfer ownership first | Yes | Yes |\n\nEach team has one owner. `transferOwnership` transfers ownership to an existing member in one transaction. The former owner becomes an admin.\n\nInvitations and direct membership grants preserve an existing role. Use `updateMemberRole` to change a non-owner's role.\n\n## Personal teams and workspace selection\n\nUse `ensurePersonalTeam` to create a user's personal team if it does not already exist. Personal ownership cannot be transferred. Accepting an invitation does not create a personal team.\n\nUse `setActiveTeam` to select the user's current workspace and `setDefaultTeam` to save their default workspace. Read these preferences with `getActiveTeam` and `getDefaultTeam`. Selecting a default team does not make it a personal team.\n\nPreferences do not grant access to your app's data. Check current membership before each protected content operation.\n\n## Seats and invitations\n\nUse `createInvite`, `resendInvite`, and `revokeInvite` to manage invitations. A duplicate pending invitation produces `INVITATION_ALREADY_PENDING`. Resending rotates the token; use the invitation ID returned by `resendInvite`.\n\nUse `acceptInvite` to grant membership to the authenticated recipient. Your backend must supply their verified email and the current `seatLimit`. Read the limit from your app's configuration in the same mutation that accepts the invitation. Apply the same policy to direct grants through `addMember`, which belongs in internal provisioning functions.\n\nThe owner consumes a seat. Pending invitations do not reserve seats. Omit `seatLimit` only for unlimited teams. A limit must be a nonnegative safe integer.\n\nConcurrent requests cannot exceed the seat limit. Duplicate grants and role changes do not consume extra seats. If a team is full, acceptance fails without consuming the invitation. Retry the same invitation after capacity changes.\n\nReducing the limit does not remove existing members. Repeated acceptance preserves the member's current role. A previously accepted invitation cannot restore a removed member.\n\n## Paginated lists\n\n`listTeams`, `listMembers`, and `listPendingInvites` accept `paginationOpts` and return `page`, `isDone`, and `continueCursor`. Request 1 to 100 items per page.\n\nPass `continueCursor` as the next request's `cursor` until `isDone` is true. Continue after an empty page too. Team pages can be empty during deletion cleanup. Team lists use membership order. Your app can sort the results for display.\n\n## Invitation delivery\n\nYour app sends invitation messages through its delivery provider. Create the invitation through an internal mutation, then send the returned token from a Convex action. See the [delivery example](https://github.com/clipinfit/convex-teams/blob/main/packages/example-backend/convex/delivery.ts).\n\nKeep raw tokens out of logs, app storage, and scheduled arguments. For scheduled delivery, schedule the recipient and team information, then create the token inside the action. Use `recordDeliveryAttempt` to record delivery status.\n\nA provider failure leaves the invitation available for explicit resend. If delivery succeeds but status recording fails, confirm the delivery state before sending again. Delivery does not have an exactly-once guarantee.\n\n## Deletion and retention\n\n`deleteTeam` requires the owner. It immediately denies team access and invalidates invitation grants. Background cleanup removes memberships and repairs affected preferences. Each user gets another team they can access, or no team.\n\nA team preference can be temporarily null during cleanup. A later explicit selection takes precedence over background repair. Cleanup retries are safe.\n\nYour app handles content deletion and subscription cancellation. Create a cleanup job in your own tables in the same mutation that calls `deleteTeam`. Reference the team by its immutable `teamPublicId`.\n\nCall `pruneInvitations` from an internal maintenance function to expire pending invitations and remove invitation records that are no longer pending after the 90-day retention period. Each call processes a bounded batch. Run enough batches for your invitation volume.\n\n## Errors and retries\n\n| Error | What to do |\n| --- | --- |\n| `Not authorized.` | Authenticate the correct user or obtain the required role or membership. Do not retry unchanged. |\n| `Team not found.` | Select another team. The team is missing or deleted. |\n| `Could not allocate a unique team slug.` | Retry team creation. |\n| `Team seat limit reached.` | Increase capacity or remove another member before retrying the invitation. |\n| `Membership no longer exists.` | Obtain a new invitation. An accepted token cannot restore removed access. |\n| `INVITATION_ALREADY_PENDING` | Use `resendInvite` to resend the pending invitation. |\n| `INVITATION_EXPIRED` or `INVITATION_REVOKED` | Obtain a new invitation. |\n| `INVITATION_AUDIENCE_MISMATCH` | Sign in with the verified email of the intended recipient. |\n| `Membership count is not ready.` | For older teams, have the owner call `prepareMembershipCount` and wait for `ready`. |\n\nConvex handles transaction conflicts. Let membership errors fail the acceptance mutation so the invitation and membership changes roll back together.\n\n## Migrate existing teams\n\nIf your app already stores teams, the import API can preserve their public IDs, names, slugs, and member roles. Use `importTeam`, `importMembers`, and `finishImport` from internal migration functions.\n\nImports do not migrate invitations, preferences, billing, or app content. See the [migration guide](https://convex-teams.vercel.app/docs/host-contract#existing-data) for the import procedure and upgrade requirements.\n\nSee the [changelog](CHANGELOG.md) for release history.\n","readmeFilename":"README.md"}