AeThex-OS/docs/MULTI_TENANCY_COMPLETE.md

300 lines
9.4 KiB
Markdown

# Multi-Tenancy Implementation Summary
## 🎯 Overview
This implementation adds full multi-tenancy support to AeThex-OS, enabling organizations, team collaboration, and project-based ownership.
---
## ✅ Deliverables Completed
### 1. Database Schema Changes (`shared/schema.ts`)
#### New Tables Added:
-**organizations** - Workspace/team containers
- `id`, `name`, `slug`, `owner_user_id`, `plan`, timestamps
-**organization_members** - Team membership
- `id`, `organization_id`, `user_id`, `role` (owner/admin/member/viewer)
- Unique constraint on (organization_id, user_id)
-**project_collaborators** - Project-level permissions
- `id`, `project_id`, `user_id`, `role`, `permissions` (jsonb)
- Unique constraint on (project_id, user_id)
- CASCADE on project deletion
#### Existing Tables Updated:
Added nullable `organization_id` column to:
-`projects` (also added `owner_user_id` for standardization)
-`aethex_projects`
-`marketplace_listings`
-`marketplace_transactions`
-`files`
-`custom_apps`
-`aethex_sites`
-`aethex_opportunities`
-`aethex_events`
All with foreign key constraints (ON DELETE RESTRICT) and indexes.
---
### 2. SQL Migrations
#### Migration 0004: Organizations & Collaborators
File: `/migrations/0004_multi_tenancy_organizations.sql`
- Creates `organizations`, `organization_members`, `project_collaborators` tables
- Adds foreign key constraints
- Creates indexes for common queries
#### Migration 0005: Organization FKs
File: `/migrations/0005_add_organization_fks.sql`
- Adds `organization_id` columns to all entity tables
- Creates foreign keys with ON DELETE RESTRICT
- Adds indexes for org-scoped queries
- Backfills `projects.owner_user_id` from existing data
#### Backfill Script
File: `/script/backfill-organizations.ts`
- Creates default organization for each existing user
- Format: `"<display_name>'s Workspace"`
- Generates unique slugs
- Adds user as organization owner
- Backfills `organization_id` for user's existing entities
---
### 3. Server Middleware (`server/org-middleware.ts`)
#### Middleware Functions:
-**attachOrgContext** - Non-blocking middleware that:
- Reads org ID from `x-org-id` header or session
- Falls back to user's first/default org
- Verifies membership and attaches `req.orgId`, `req.orgRole`
-**requireOrgMember** - Blocks requests without org membership
-**requireOrgRole(minRole)** - Enforces role hierarchy (viewer < member < admin < owner)
#### Helper Functions:
- **assertProjectAccess(projectId, userId, minRole)** - Checks:
- Project ownership
- Collaborator role
- Organization membership (if project is in an org)
---
### 4. Server API Routes (`server/routes.ts`)
#### Organization Routes:
- `GET /api/orgs` - List user's organizations
- `POST /api/orgs` - Create new organization (auto-adds creator as owner)
- `GET /api/orgs/:slug` - Get organization details (requires membership)
- `GET /api/orgs/:slug/members` - List organization members (requires membership)
#### Project Routes (Updated):
- `GET /api/projects` - Org-scoped list (admin sees all, users see org projects)
- `GET /api/projects/:id` - Access-controlled project fetch
- `GET /api/projects/:id/collaborators` - List collaborators (requires contributor role)
- `POST /api/projects/:id/collaborators` - Add collaborator (requires admin role)
- `PATCH /api/projects/:id/collaborators/:collabId` - Update role/permissions (requires admin)
- `DELETE /api/projects/:id/collaborators/:collabId` - Remove collaborator (requires admin)
#### Middleware Application:
```typescript
app.use("/api/orgs", requireAuth, attachOrgContext);
app.use("/api/projects", attachOrgContext);
app.use("/api/files", attachOrgContext);
app.use("/api/marketplace", attachOrgContext);
```
---
### 5. Client Components
#### OrgSwitcher Component (`client/src/components/OrgSwitcher.tsx`)
- Dropdown menu in top nav
- Lists user's organizations
- Shows current org with checkmark
- Stores selection in localStorage
- Provides hooks:
- `useCurrentOrgId()` - Get active org ID
- `useOrgHeaders()` - Get headers for API calls
#### Organizations List Page (`client/src/pages/orgs.tsx`)
- View all user's organizations
- Create new organization with name + slug
- Auto-generates slug from name
- Shows user's role per org
- Navigation to settings
#### Organization Settings Page (`client/src/pages/orgs/settings.tsx`)
- Tabbed interface: General | Members
- **General Tab:**
- Display org name, slug, plan
- (Edit capabilities noted as "coming soon")
- **Members Tab:**
- List all members with avatars
- Show roles with colored badges + icons
- Owner (purple/crown), Admin (cyan/shield), Member (slate/user), Viewer (slate/eye)
#### Routes Added to App.tsx:
```tsx
<Route path="/orgs">{() => <ProtectedRoute><Orgs /></ProtectedRoute>}</Route>
<Route path="/orgs/:slug/settings">{() => <ProtectedRoute><OrgSettings /></ProtectedRoute>}</Route>
```
---
### 6. Documentation
#### README_EXPANSION.md Updated
- Added section: "Multi-Tenancy & Project Ownership"
- Documented difference between `projects` and `aethex_projects`:
- **projects**: Canonical internal project graph, org-scoped, full collaboration
- **aethex_projects**: Public showcase/portfolio, creator-focused
- Outlined future migration plan to link the two
---
## 🔧 Usage Guide
### For Developers
#### Running Migrations:
```bash
# Apply migrations
npx drizzle-kit push
# Run backfill script
npx tsx script/backfill-organizations.ts
```
#### Making Org-Scoped API Calls (Client):
```tsx
import { useOrgHeaders } from "@/components/OrgSwitcher";
function MyComponent() {
const orgHeaders = useOrgHeaders();
const { data } = useQuery({
queryKey: ["/api/projects"],
queryFn: async () => {
const res = await fetch("/api/projects", {
credentials: "include",
headers: orgHeaders, // Adds x-org-id header
});
return res.json();
},
});
}
```
#### Checking Project Access (Server):
```typescript
import { assertProjectAccess } from "./org-middleware.js";
app.get("/api/projects/:id/some-action", requireAuth, async (req, res) => {
const accessCheck = await assertProjectAccess(
req.params.id,
req.session.userId!,
'contributor' // minimum required role
);
if (!accessCheck.hasAccess) {
return res.status(403).json({ error: accessCheck.reason });
}
// Proceed with action
});
```
---
## 🚀 What's Next (Future Work)
### Phase 2: Full Org Scoping
- Scope `aethex_sites`, `aethex_opportunities`, `aethex_events` list endpoints
- Add org filtering to admin dashboards
- Implement org-wide analytics
### Phase 3: Advanced Permissions
- Granular permissions matrix (read/write/delete per resource)
- Project templates and cloning
- Org-level roles (billing admin, content moderator, etc.)
### Phase 4: Billing & Plans
- Integrate Stripe for org subscriptions
- Enforce feature limits per plan (free/pro/enterprise)
- Usage metering and billing dashboard
### Phase 5: Invitations & Discovery
- Email-based invitations to join organizations
- Invite links with tokens
- Public org directory (for discoverability)
- Transfer ownership flows
---
## 📊 Architecture Decisions
### Why Nullable `organization_id` First?
- **Safety**: Existing data remains intact
- **Gradual Migration**: Users can operate without orgs initially
- **Backfill-Ready**: Script can populate later without breaking changes
### Why RESTRICT on Delete?
- **Data Safety**: Accidental org deletion won't cascade delete all projects
- **Audit Trail**: Forces manual cleanup or archive before deletion
- **Future Proof**: Can implement "soft delete" or "archive" later
### Why Separate `project_collaborators`?
- **Flexibility**: Collaborators can differ from org members
- **Granular Control**: Project-level permissions independent of org roles
- **Cross-Org Collaboration**: Future support for external collaborators
### Why Keep Legacy Columns (`user_id`, `owner_id`)?
- **Backwards Compatibility**: Existing code paths still work
- **Migration Safety**: Can validate new columns before removing old ones
- **Rollback Path**: Easy to revert if issues found
---
## ✅ Testing Checklist
- [ ] Run migrations on clean database
- [ ] Run backfill script with existing user data
- [ ] Create organization via UI
- [ ] Invite member to organization (manual DB insert for now)
- [ ] Switch between orgs using OrgSwitcher
- [ ] Verify projects are scoped to selected org
- [ ] Add collaborator to project
- [ ] Verify access control (viewer vs admin)
- [ ] Check that legacy `projects` queries still work (admin routes)
---
## 📝 Migration Checklist for Production
1. **Pre-Migration:**
- [ ] Backup database
- [ ] Review all foreign key constraints
- [ ] Test migrations on staging
2. **Migration:**
- [ ] Run 0004_multi_tenancy_organizations.sql
- [ ] Run 0005_add_organization_fks.sql
- [ ] Run backfill-organizations.ts script
- [ ] Verify all users have default org
3. **Post-Migration:**
- [ ] Verify existing projects still accessible
- [ ] Check org member counts
- [ ] Test org switching in UI
- [ ] Monitor for access control issues
4. **Cleanup (Later):**
- [ ] Once validated, make `organization_id` NOT NULL
- [ ] Drop legacy columns (`user_id`, `owner_id` from projects)
- [ ] Update all queries to use new columns
---
**Status:** Multi-tenancy foundation complete and ready for use.