# Phase 3e: Organization Support Implementation Plan
**Created**: 2026-01-03
**Status**: Planning Complete - Ready for Implementation
---
## Executive Summary
Add multi-tenant organization support to the MCP server template. Organizations allow:
- Teams/workspaces to share MCP server access
- Role-based access control (owner/admin/member)
- Future: org-scoped API keys and shared services
---
## Current State Analysis
### What Exists
| Component | Status | Notes |
|-----------|--------|-------|
| `auth.ts` organization plugin | ✅ Configured | Teams enabled, invitations configured |
| `schema.ts` organization tables | ⚠️ Partial | Missing team/teamMember tables |
| D1 organization table | ✅ Exists | From 0005 migration |
| D1 member table | ✅ Exists | From 0005 migration |
| D1 invitation table | ✅ Exists | From 0005 migration |
| D1 session.activeOrganizationId | ❌ Missing | Required for org context |
| D1 team/teamMember tables | ❌ Missing | Required since teams enabled |
| Organization UI | ❌ Missing | No admin dashboard section |
| Organization API routes | ❌ Missing | better-auth provides, not exposed |
### Gap Analysis
1. **Database Schema Gaps**:
- `session` table missing `active_organization_id` column
- `session` table missing `active_team_id` column
- `team` table doesn't exist
- `team_member` table doesn't exist
2. **Code Gaps**:
- `schema.ts` missing team/teamMember definitions
- No organization UI in admin dashboard
- No org-aware middleware
- better-auth org endpoints not explicitly exposed
---
## Implementation Decisions
### Decision 1: Teams Feature
**Question**: Keep teams enabled or simplify to organizations-only?
**Decision**: **Keep teams enabled**
- Already configured in auth.ts
- Useful for larger organizations
- Only adds 2 tables
- Can be hidden from UI initially if too complex
### Decision 2: Organization UI Location
**Question**: Where should org management live?
**Decision**: **Admin dashboard with user self-service**
- Admins see all orgs + can manage any
- Users see their orgs + can create new
- Separate "Organizations" tab in dashboard
### Decision 3: Invitation System
**Question**: Use invitations or direct member adding?
**Decision**: **Both**
- Invitations for external users (email-based)
- Direct add for existing users (admin only)
- Console.log placeholder for emails (Phase 3h: proper email)
### Decision 4: Role System
**Question**: Use default roles or custom RBAC?
**Decision**: **Default roles for now**
- owner: Full control
- admin: Manage members, no delete org
- member: Read-only
Custom RBAC adds complexity - defer to future phase if needed.
---
## Database Changes
### Migration 0006: Organization Support
```sql
-- Add session columns for org context
ALTER TABLE session ADD COLUMN active_organization_id TEXT;
ALTER TABLE session ADD COLUMN active_team_id TEXT;
-- Team table
CREATE TABLE team (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
organization_id TEXT NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Team member table
CREATE TABLE team_member (
id TEXT PRIMARY KEY NOT NULL,
team_id TEXT NOT NULL REFERENCES team(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL,
UNIQUE(team_id, user_id)
);
-- Indexes
CREATE INDEX idx_team_org ON team(organization_id);
CREATE INDEX idx_team_member_team ON team_member(team_id);
CREATE INDEX idx_team_member_user ON team_member(user_id);
```
---
## Schema Updates (schema.ts)
### Add to session table:
```typescript
export const session = sqliteTable('session', {
// ... existing columns
activeOrganizationId: text('active_organization_id'),
activeTeamId: text('active_team_id'),
});
```
### Add new tables:
```typescript
export const team = sqliteTable('team', {
id: text('id').primaryKey(),
name: text('name').notNull(),
organizationId: text('organization_id')
.notNull()
.references(() => organization.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const teamMember = sqliteTable('team_member', {
id: text('id').primaryKey(),
teamId: text('team_id')
.notNull()
.references(() => team.id, { onDelete: 'cascade' }),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});
```
---
## API Endpoints
### better-auth Auto-Generated (Already Available)
These work automatically via `/api/auth/*`:
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/auth/organization/create` | POST | Create organization |
| `/api/auth/organization/list` | GET | List user's organizations |
| `/api/auth/organization/get-full` | GET | Get org with members |
| `/api/auth/organization/update` | PUT | Update organization |
| `/api/auth/organization/delete` | DELETE | Delete organization |
| `/api/auth/organization/set-active` | POST | Set active org in session |
| `/api/auth/organization/invite-member` | POST | Send invitation |
| `/api/auth/organization/accept-invitation` | POST | Accept invitation |
| `/api/auth/organization/list-members` | GET | List org members |
| `/api/auth/organization/update-member-role` | PUT | Change role |
| `/api/auth/organization/remove-member` | DELETE | Remove member |
### Custom Admin Endpoints (To Implement)
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/admin/organizations` | GET | List ALL orgs (admin only) |
| `/api/admin/organizations/:id` | GET | Get any org details |
| `/api/admin/organizations/:id` | DELETE | Delete any org |
---
## UI Design
### Organizations Section in Admin Dashboard
```
┌─────────────────────────────────────────────────────────────────┐
│ Organizations [+ Create] │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Your Organizations │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Jezweb Team owner 3 members [Manage] │ │ │
│ │ │ slug: jezweb-team │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Test Org admin 1 member [Manage] │ │ │
│ │ │ slug: test-org │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Pending Invitations │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Acme Corp - invited as member [Accept] [Decline] │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### Organization Detail Modal
```
┌─────────────────────────────────────────────────────────────────┐
│ Jezweb Team [Close] │
├─────────────────────────────────────────────────────────────────┤
│ Settings │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Name: [Jezweb Team____________] │ │
│ │ Slug: jezweb-team (read-only) │ │
│ │ Logo: [Upload] │ │
│ │ [Save Changes] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Members [+ Invite] │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ jeremy@jezweb.net Owner You │ │
│ │ john@jezweb.net Admin [▼ Role] [Remove] │ │
│ │ support@jezweb.net Member [▼ Role] [Remove] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Pending Invitations │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ newuser@example.com Member Expires: 2d [Cancel] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ [Delete Organization] │
└─────────────────────────────────────────────────────────────────┘
```
---
## Implementation Steps
### Step 1: Database Migration (30 min)
1. Create `migrations/0006_organization_support.sql`
2. Apply to local: `wrangler d1 execute --local --file=...`
3. Apply to remote: `wrangler d1 execute --remote --file=...`
### Step 2: Schema Updates (15 min)
1. Add `activeOrganizationId` and `activeTeamId` to session in schema.ts
2. Add `team` and `teamMember` tables to schema.ts
3. Add to auth.ts adapter schema mapping
### Step 3: Admin API Routes (45 min)
1. Add `/api/admin/organizations` GET endpoint
2. Add org-aware context to admin middleware
3. Test better-auth org endpoints work
### Step 4: Organizations UI (1.5 hr)
1. Add Organizations tab to admin dashboard
2. Create organization list component
3. Create organization detail/manage modal
4. Create invite member modal
5. Create create organization modal
6. Handle pending invitations display
### Step 5: Testing (30 min)
1. Test create organization
2. Test invite member flow
3. Test role changes
4. Test org deletion
5. Test multi-org user
### Step 6: Documentation (15 min)
1. Update SESSION.md
2. Update BETTER_AUTH_ARCHITECTURE.md if needed
---
## Files to Modify/Create
| File | Action | Description |
|------|--------|-------------|
| `migrations/0006_organization_support.sql` | CREATE | Add session columns + team tables |
| `src/lib/db/schema.ts` | MODIFY | Add team tables, session columns |
| `src/lib/auth.ts` | MODIFY | Add team/teamMember to adapter |
| `src/admin/routes.ts` | MODIFY | Add org admin endpoints |
| `src/admin/ui.ts` | MODIFY | Add Organizations section |
---
## Future Considerations (Not This Phase)
1. **Org-scoped API Keys**: API keys belong to org, not user
2. **Org-scoped Shared Services**: Layer 2 services per org
3. **Custom Roles**: RBAC beyond owner/admin/member
4. **Email Sending**: Real email for invitations (use Resend/SendGrid)
5. **Org Switching in MCP**: X-Organization-Id header for tool context
---
## Verification Criteria
- [ ] Can create organization with name and slug
- [ ] Can list user's organizations
- [ ] Can invite member by email (console.log ok)
- [ ] Can accept/reject invitation
- [ ] Can change member role
- [ ] Can remove member (except last owner)
- [ ] Can delete organization (owner only)
- [ ] Session tracks active organization
- [ ] Admin can see all organizations