Adding a Team plan (we call it “Crew”) is one of the highest‑leverage levers for a product graduating from solo users to real collaboration. It changes your pricing, accelerates growth loops, removes a latent blocker for buyers, and—done right—doesn’t require a rewrite.
Here’s how we approached it in Indie10k, what we learned, and how you can ship it without derailing your roadmap.
Why a Team Plan Matters
- Momentum spreads in groups. When collaborators see each other’s progress, they keep pace.
- Expansion revenue beats pure acquisition. Teams raise ARPU and unlock account‑level upgrades.
- The “but my co‑founder needs access” objection disappears once you support basic roles.
Pricing Model: From Individual to Account Seats
We priced Crew around an account‑level seat pool, not per‑project invites.
- Account seats represent unique collaborators across all owned projects.
- Pending invites consume seats; revoking frees them immediately.
- The owner doesn’t count against the pool, and the same collaborator on multiple projects counts once.
This strikes a balance between predictable revenue and flexibility for small teams or collectives. For Indie10k:
- Free/Builder/Hacker: solo or limited collaboration (0 seat pool by default)
- Crew: includes a pooled seat allowance (e.g., 3 seats)
Env example:
ENABLE_TEAM=true
TEAM_DEFAULT_SEAT_POOL=0
TEAM_CREW_SEAT_POOL=3
TEAM_INVITE_EXPIRES_DAYS=7
Tip: Map seats to paid tiers via your billing mapping rather than baking prices into code.
Growth Effects: ARPU, Viral Loops, Retention
- Higher ARPU: Accounts upgrade to unlock more collaborators; seat caps create natural upsell moments.
- Product‑led invites: Each invite is a low‑friction acquisition touchpoint; acceptance funnels new users into your product.
- Retention via accountability: Teams return to close loops together (status checks, evidence sharing, and reviews).
Instrumentation ideas:
- Track seat‑cap‑reached events as an upsell signal.
- Measure invite acceptance rate and time‑to‑first‑action post‑accept.
- Segment retention by “solo vs team” to quantify the impact.
Pain It Solves (Real Quotes We Heard)
- “I can’t add my co‑founder without sharing my login.”
- “We split projects between personal accounts—now everything’s scattered.”
- “We need read‑only for the advisor and write for the dev.”
Answer: roles (reader, writer, admin, owner) with predictable behavior and a simple members UI.
How To Ship It (Fast, Safely)
The path we used—minimal, reversible, and feature‑flagged.
1) Start With a Tight PRD
Keep scope lean: invites by email, roles, a members panel, seat enforcement, and secure tokens. Our full PRD is in docs/team-prd.md
and covers:
- Roles matrix and non‑goals for v1.
- Account‑level seat pool rules.
- Security (hashed, expiring, single‑use tokens; no email enumeration).
2) Schema + Services (Server‑First)
Add two tables:
project_members(project_id, user_id, role, status, …)
project_invites(project_id, email, role, token_hash, expires_at, status, …)
Service helpers (server‑only):
requireRole(projectId, userId, minRole)
createInvite(projectId, email, role, invitedBy)
acceptInvite(token, userId)
-
changeMemberRole(...)
,removeMember(...)
,revokeInvite(inviteId)
- Seat math:
getSeatPoolSize(ownerId)
+getSeatsUsed(ownerId)
(distinct users + pending emails across all owner projects)
3) APIs That Mirror the Mental Model
-
POST /api/projects/:id/invites
→ create invite (admin+) -
POST /api/team/invites/accept
→ accept invite (auth) -
PATCH /api/projects/:id/members/:userId
→ change role (admin+) -
DELETE /api/projects/:id/members/:userId
→ remove (admin+) -
DELETE /api/projects/:id/invites/:inviteId
→ revoke (admin+) -
POST /api/projects/:id/invites/:inviteId/resend
→ rotate token + resend (admin+, rate‑limited) -
GET /api/projects/:id/members
→ list members + pending invites + seat usage
Guard every route with server‑side role checks. Keep responses predictable: NextResponse.json({ ... }, { status })
.
4) Email + Accept Flow
- Invite email includes an accept URL:
/invite/accept?token=...
. - The page calls
POST /api/team/invites/accept
and redirects to the project. - In dev:
RESEND_API_KEY=STDOUT
to safely preview emails in logs.
5) Members UI That Doesn’t Overreach
- A members list with roles and a simple invite form.
- A seat meter that reads “X of Y seats used (account‑wide)”.
- Pending invites with “Resend” and “Revoke”.
- Show names/emails instead of raw IDs for clarity.
6) Ratchets, Not Roadblocks
Use rate limits where abuse is likely (invite create/resend). Fail‑open on missing infra in dev.
// Example keys
invite:create:user:{userId}
invite:resend:user:{userId}
invite:resend:project:{projectId}
7) Secure by Default
- Hash tokens (sha256 with a salt) and store only the hash.
- Enforce expiry on accept; treat missing keys as 500 in production.
- Avoid email enumeration; always respond with a generic “Invite sent”.
Example PRD (Summary)
Roles: reader, writer, admin, owner. Readers view; writers edit and run actions; admins manage members; owner is immutable and can delete the project.
Seat Pool: unique collaborators across owner’s projects; pending invites count; revokes free seats immediately.
UI: Members list, invite form, seat meter, and clear error states.
Reference: see docs/team-prd.md
for the complete matrix and constraints.
Implementation Guide (Summary)
We documented the exact stack‑aligned steps (Drizzle schema, App Router routes, Zod validators, Resend emails, Stack Auth checks) in docs/team-impl.md
.
Highlights:
- Feature flag:
ENABLE_TEAM
hides everything until you’re ready. - Role checks: centralized with
requireRole()
and reused across routes. - Access scope: extend project listings and fetches to include active members (not just owners), while locking down mutations to writer/admin.
Final Notes
If you’ve got even a handful of users asking “Can I invite my co‑founder?”, a Team plan unlocks not just collaboration—but an entire business model path: expansion revenue, stickier usage, and more reliable growth.
Ship it small. Lock it down. Iterate from there.
Top comments (0)