Team & Billing Architecture Redesign
Summary
Redesign the billing model to use a unified "Personal Team" pattern where all billing flows through teams, with support for: - Monthly budget caps per team member - Existing users joining teams while keeping their personal accounts - Email invitations via Resend
Current Problems
- Dual balance confusion: Both
User.balanceCentsandTeam.balanceCentsexist - No budget control: Team owners can't limit member spending
- No email invites: Team invitation system not implemented
- Unclear team creation: Teams only created during Stripe flow
Proposed Architecture
User Registration Flow
Team Invitation Flow
Data Model
Schema Changes
Remove User.balanceCents
model User {
// REMOVE: balanceCents Int @default(0)
// Balance now lives only on Team
}
Update TeamMember with Budget Controls
model TeamMember {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role TeamRole @default(MEMBER)
rateLimitRpm Int @default(0)
// NEW: Monthly budget controls
monthlyBudgetCents Int? // null = unlimited (uses team balance)
usedThisMonthCents Int @default(0)
budgetResetAt DateTime? // When to reset usage counter
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@unique([userId, teamId])
}
Add Active Team to User
model User {
// ... existing fields
// NEW: Track which team the user is currently using
activeTeamId String?
activeTeam Team? @relation("ActiveTeam", fields: [activeTeamId], references: [id])
}
Edge Cases
Case 1: New User Signs Up
- User creates account via OAuth
- System auto-creates "Personal Team" with user as owner
- Team starts with $0 balance
- User adds funds via Stripe → credits Team.balanceCents
Case 2: Existing User Invited to Another Team
- User A (owner) invites User B (existing account)
- User B receives email invitation
- User B clicks link, accepts invitation
- User B now belongs to TWO teams:
- Their personal team (still exists, still has their balance)
- User A's team (as a member)
- User B can switch between teams in the UI
- API calls use the
activeTeamIdto determine billing
Case 3: User Hits Monthly Budget
- TeamMember has
monthlyBudgetCents: 5000($50) - User makes API calls,
usedThisMonthCentsincrements - When
usedThisMonthCents >= monthlyBudgetCents: - API returns 402 "Monthly budget exceeded"
- UI shows red indicator
- Team owner can increase budget anytime
- On
budgetResetAt, system resetsusedThisMonthCentsto 0
Case 4: 5 Individual Users Join One Team
| User | Before | After |
|---|---|---|
| A (manager) | Has personal team with $100 | Creates/uses "Company Team" |
| B | Has personal team with $20 | Joins Company Team, keeps personal team |
| C | Has personal team with $0 | Joins Company Team, keeps personal team |
| D | Has personal team with $50 | Joins Company Team, keeps personal team |
| E | New user | Gets invite, signs up, joins Company Team |
- Each user's personal balance remains untouched
- Company Team has its own balance (funded by A)
- When users work in Company Team, it debits from Company Team balance
- Users can switch to personal team to use their own balance
API Changes
GET /api/user/status
// Response now includes active team info
{
email: string,
name: string,
hasSubscription: boolean, // activeTeam.balanceCents > 0
apiKey: string,
activeTeam: {
id: string,
name: string,
balanceCents: number,
role: "ADMIN" | "MEMBER"
},
teams: [...] // All teams user belongs to
}
POST /api/team/switch
Switch the user's active team:
// Request
{ teamId: string }
// Response
{ success: true, activeTeam: {...} }
POST /api/team/invite
Send email invitation:
// Request
{
email: string,
role?: "ADMIN" | "MEMBER",
monthlyBudgetCents?: number
}
// Response
{ success: true, invitationId: string }
POST /api/team/member/budget
Update member's monthly budget:
// Request
{
memberId: string,
monthlyBudgetCents: number | null // null = unlimited
}
Email Setup (Resend)
Why Resend over GSuite
| Feature | Resend | GSuite |
|---|---|---|
| Purpose | Transactional emails | Business email |
| Pricing | Free: 100/day, $20/mo: 50k | $6/user/month |
| Setup | API key, 5 minutes | DNS, admin console |
| Best for | Invites, notifications | Human correspondence |
Implementation
npm install resend
// lib/email.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendTeamInvite(email: string, teamName: string, inviteUrl: string) {
await resend.emails.send({
from: 'Fabric <noreply@codewithfabric.com>',
to: email,
subject: `You've been invited to join ${teamName} on Fabric`,
html: `...`
});
}
DNS Records for codewithfabric.com
Add these records in your DNS provider:
Type: TXT
Name: @
Value: v=spf1 include:resend.com ~all
Type: TXT
Name: resend._domainkey
Value: (provided by Resend dashboard)
Migration Plan
Step 1: Schema Migration
- Add new fields to TeamMember (budget controls)
- Add activeTeamId to User
- Keep User.balanceCents temporarily (for migration)
Step 2: Data Migration
-- For each user without a team, create a personal team
INSERT INTO "Team" (id, name, "ownerId", "balanceCents", ...)
SELECT
gen_random_uuid(),
name || '''s Team',
id,
"balanceCents", -- Move user balance to team
...
FROM "User" u
WHERE NOT EXISTS (SELECT 1 FROM "Team" t WHERE t."ownerId" = u.id);
-- Set activeTeamId to their personal team
UPDATE "User" u
SET "activeTeamId" = (SELECT id FROM "Team" t WHERE t."ownerId" = u.id LIMIT 1);
Step 3: Update Billing Logic
- Update
/api/user/statusto use activeTeam.balanceCents - Update
/api/keys/generatesimilarly - Update LiteLLM callback to debit from activeTeam
Step 4: Remove User.balanceCents
After verifying migration:
model User {
// REMOVE: balanceCents Int @default(0)
}
Files to Modify
| File | Changes |
|---|---|
prisma/schema.prisma |
Add budget fields, activeTeamId, remove User.balanceCents |
src/app/api/user/status/route.ts |
Use activeTeam for billing check |
src/app/api/keys/generate/route.ts |
Use activeTeam balance |
src/app/api/team/invite/route.ts |
NEW: Send email invitations |
src/app/api/team/switch/route.ts |
NEW: Switch active team |
src/app/api/team/member/budget/route.ts |
NEW: Update member budget |
src/app/dashboard/team/page.tsx |
Add budget controls UI |
src/lib/email.ts |
NEW: Resend integration |
src/lib/litellm.ts |
Update to check member budget |
Test Scenarios
- New user signup → personal team created
- User adds funds → team balance increases
- User A invites User B → email sent, B can accept
- User B accepts → B in both teams, can switch
- Owner sets $50 budget for member → enforced on API calls
- Member exceeds budget → 402 error, red UI indicator
- Owner increases budget → member can continue
- Month ends → budget resets automatically


