AeThex-OS/MULTI_TENANCY_COMPLETE.md
MrPiglr 4b84eedbd3 feat: Add revenue_events table and fix migration FK constraints
- Add revenue_events table to track org/project revenue with source tracking
- Add Drizzle schema for revenue_events with proper org/project references
- Create migration 0006_revenue_events.sql with indexes
- Fix migration 0004: Remove FK constraints to profiles.id (auth schema incompatibility)
- Document auth.users/profiles.id type mismatch (UUID vs VARCHAR)
- Harden profile update authorization (self-update or org admin/owner only)
- Complete org-scoping security audit implementation (42 gaps closed)
2026-01-05 04:54:12 +00:00

9.4 KiB

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:

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:

<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:

# Apply migrations
npx drizzle-kit push

# Run backfill script
npx tsx script/backfill-organizations.ts

Making Org-Scoped API Calls (Client):

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):

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.