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:
- ✅
project table
- ✅
task table
- ✅
worker table
- ✅
audit_log table
# 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_id in 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 /workers endpoints
- All
POST, PUT, DELETE operations (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
Phase 2: Default Organization
Phase 3: Query Filtering
Phase 4: Security Validation
Phase 5: Documentation
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 has tenant_id)
Note: ChatKit store already uses organization_id - consider aligning both to tenant_id for consistency.
Related
Questions for Discussion
- Default org creation timing: Signup vs first API request?
- Default org name: "Personal Workspace" or user's name?
- Rename ChatKit
organization_id to tenant_id for consistency?
- Migration strategy: Assign to first org or create default per user?
🤖 Generated with Claude Code
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_idto core models and filter all queries by the authenticated user's active tenant from JWT.1. Database Schema Changes
Add
tenant_idcolumn to:projecttabletasktableworkertableaudit_logtable2. 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:Alternative: During signup - SSO creates default org immediately
Benefits:
3. Query Filtering
Add tenant scoping to ALL queries:
Where to apply:
GET /projects,GET /tasks,GET /workersendpointsPOST,PUT,DELETEoperations (verify tenant ownership)4. Middleware for tenant_id Extraction
5. Migration Strategy
For existing data:
Implementation Checklist
Phase 1: Schema & Migration
tenant_idcolumn to Project, Task, Worker, AuditLog modelstenant_idcolumnsPhase 2: Default Organization
Phase 3: Query Filtering
Phase 4: Security Validation
Phase 5: Documentation
Impact Analysis
Security
UX
Default Org Behavior
Performance
Naming: tenant_id vs organization_id?
Recommendation:
tenant_idWhy:
tenant_id)activeOrganizationId→ JWT hastenant_id)Note: ChatKit store already uses
organization_id- consider aligning both totenant_idfor consistency.Related
/sp.specifyfor multi-tenancy backend)Questions for Discussion
organization_idtotenant_idfor consistency?🤖 Generated with Claude Code