-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem Statement
Current State: Backend API (Taskflow API) has no organization/tenant scoping. All users see all projects/tasks regardless of which organization they're in via the org switcher.
Security Risk: 🔴 CRITICAL - Without tenant isolation, users can access each other's data.
UX Issue: Users switch organizations in the UI but see the same data (confusing and broken).
Proposed Solution
Add tenant_id to core models and filter all queries by the authenticated user's active tenant from JWT.
1. Database Schema Changes
Add tenant_id column to:
- ✅
projecttable - ✅
tasktable - ✅
workertable - ✅
audit_logtable
# Example: Project model
class Project(SQLModel, table=True):
# ... existing fields ...
tenant_id: str = Field(
index=True,
description="Organization ID from SSO (tenant isolation)"
)2. Default Organization Strategy
Problem: Users shouldn't need to understand organizations to use Taskflow.
Solution: Auto-create "Personal Workspace" as default organization.
When to Create Default Org:
-
First API request - If user has no
tenant_idin JWT:- Call SSO API to create "Personal Workspace" org
- SSO assigns user as owner
- SSO updates session with new tenant_id
- Redirect user to retry request (new JWT with tenant_id)
-
Alternative: During signup - SSO creates default org immediately
Benefits:
- ✅ Seamless single-user experience
- ✅ No need to explain organizations upfront
- ✅ Users can add orgs later as they grow
- ✅ Zero setup friction
3. Query Filtering
Add tenant scoping to ALL queries:
# Before (INSECURE)
projects = session.exec(select(Project)).all()
# After (SECURE)
tenant_id = get_tenant_from_jwt(request)
projects = session.exec(
select(Project).where(Project.tenant_id == tenant_id)
).all()Where to apply:
- All
GET /projects,GET /tasks,GET /workersendpoints - All
POST,PUT,DELETEoperations (verify tenant ownership) - Audit log queries
4. Middleware for tenant_id Extraction
async def inject_tenant_id(request: Request, call_next):
"""Extract tenant_id from JWT and attach to request state."""
jwt_claims = decode_jwt(request.cookies.get("taskflow_id_token"))
tenant_id = jwt_claims.get("tenant_id")
if not tenant_id:
# User has no org - trigger default org creation
return await create_default_org_flow(request)
request.state.tenant_id = tenant_id
return await call_next(request)5. Migration Strategy
For existing data:
-- Option 1: Assign to user's first organization
UPDATE project p
SET tenant_id = (
SELECT organization_id
FROM member
WHERE user_id = p.owner_id
LIMIT 1
);
-- Option 2: Create default org for each user, assign all their data
-- (Safer - preserves all data under user's control)Implementation Checklist
Phase 1: Schema & Migration
- Add
tenant_idcolumn to Project, Task, Worker, AuditLog models - Create migration script (Alembic)
- Backfill existing data with default org per user
- Add indexes on
tenant_idcolumns
Phase 2: Default Organization
- SSO: Create default "Personal Workspace" org on user signup
- OR: Backend middleware creates default org on first API request
- Handle edge case: user deletes all orgs (recreate default)
Phase 3: Query Filtering
- Add tenant_id extraction middleware
- Update ALL query endpoints to filter by tenant_id
- Update ALL mutations to verify tenant ownership
- Add tenant_id to audit logs
Phase 4: Security Validation
- Test: User A cannot see User B's projects
- Test: User A cannot modify User B's tasks
- Test: Switching orgs shows different data
- Test: Default org created automatically
Phase 5: Documentation
- Update API docs with tenant scoping behavior
- Document default org creation flow
- Add migration guide for existing deployments
Impact Analysis
Security
- Before: 🔴 All users see all data (tenant_id ignored)
- After: ✅ Complete data isolation per organization
UX
- Before: Org switcher doesn't work (data unchanged)
- After: ✅ Switching orgs shows that org's data
Default Org Behavior
- Single user: ✅ Never sees organizations, just works
- Team user: ✅ Can switch between orgs seamlessly
- Growth path: ✅ Start alone, invite team later
Performance
- Query impact: Minimal (indexed tenant_id filter)
- Migration: One-time cost to backfill existing data
Naming: tenant_id vs organization_id?
Recommendation: tenant_id
Why:
- ✅ Matches JWT claim name (
tenant_id) - ✅ Standard multi-tenancy terminology
- ✅ Consistent with SSO (session has
activeOrganizationId→ JWT hastenant_id)
Note: ChatKit store already uses organization_id - consider aligning both to tenant_id for consistency.
Related
- Closes organization data isolation requirement
- Depends on: SSO organization switching (feat(organizations): Complete Organization Management UI #19)
- Spec: TBD (needs
/sp.specifyfor multi-tenancy backend)
Questions for Discussion
- Default org creation timing: Signup vs first API request?
- Default org name: "Personal Workspace" or user's name?
- Rename ChatKit
organization_idtotenant_idfor consistency? - Migration strategy: Assign to first org or create default per user?
🤖 Generated with Claude Code