Teams and roles
Three roles per project — owner, member, billing — with email invites, invite links and a domain allow-list.
A project is not a lonely account. Add people under Teams and each one gets a seat on that project with a role that decides what the seat can touch. Membership is per project: someone can be an owner on one app and a member on another, and the Teams screen shows the seat matrix across every project in the workspace.
The three roles
| Role | What the seat can do |
|---|---|
| owner | Everything: deploy, database and secrets; manage domains, API keys and deploy hooks; change team settings, roles and memberships; pick alert recipients; delete the project. |
| member | The build work: deploys, database, secrets and env, logs, monitors, errors. Can invite other members — but only members. |
| billing | Money, nothing else: plan, card, credits and invoices. Cannot deploy or read the vault. |
In API terms the routes check three buckets: owner alone for destructive and team-admin actions, owner + member for build surfaces, and owner + billing for billing. Every seat can open the project and read its dashboard.
Email invites
Invite by email address and role. The invite link lands in their inbox and is valid for 7 days; accepting requires signing in with the invited address, so a forwarded link does not grant a seat to the wrong person.
- Resend mints a fresh token and restarts the 7-day clock — use it when the first mail expired or went to spam.
- Revoke kills a pending invite before it is accepted.
- Only an owner can grant the owner or billing roles; a member's invites are members-only by construction.
Invite links and domain lock
For a team where inviting one address at a time does not scale, the project has an invite link: https://camplax.dev/join/<token>. Enable it in team settings and anyone signed in who opens it joins as a member. Rotate the token and every old link dies at once — that is the off-switch when a link leaks.
Two settings control who may join at all:
- Allowed domains — a list like
acme.com; only matching email addresses can be invited or use the link. - Domain lock — when on, the allow-list is enforced (invites to other domains come back
blocked; the join link refuses them). With the lock on and an empty list, nothing gets in — the list is the gate, not a suggestion.
The rules that protect you
- The last owner cannot be demoted or removed. Demoting or deleting the final
ownerseat returnslast_owner— a project always keeps someone who can administer it. - You cannot remove yourself. Another owner does it.
- Invites to domains outside an active lock are recorded as
blocked, not sent — you can see who was turned away. - All team endpoints are session-only: project API keys cannot manage membership.
The API surface
| Endpoint | What it does | Who |
|---|---|---|
GET /v1/projects/:slug/teams | Members, pending invites, role counts, invite-link state | any seat |
PATCH /v1/projects/:slug/teams/settings | domainLock, allowedDomains, inviteLinkEnabled | owner |
POST /v1/projects/:slug/teams/invite-link/rotate | New link token; old links stop working | owner |
POST /v1/projects/:slug/teams/invites | { "emails": […], "role" } — email invites | owner/member |
POST /v1/projects/:slug/teams/invites/accept | Accept by token; session email must match | the invitee |
POST /v1/projects/:slug/teams/invites/:id/resend | New token + fresh 7 days | owner/member |
DELETE /v1/projects/:slug/teams/invites/:id | Revoke a pending invite | owner/member |
PATCH /v1/projects/:slug/teams/members/:id | Change a role | owner |
DELETE /v1/projects/:slug/teams/members/:id | Remove a seat | owner |
POST /v1/projects/:slug/teams/join | Join via an invite-link token | any signed-in user the lock allows |