7.2 KiB
Migration Guide
Overview
This document describes how products (Bermooda, Hamsoo, new products) connect to the Identity Platform, and how the platform evolved from a product-coupled UserManager to an independent identity service.
From UserManager → Central Identity Platform
Phase 1: Architecture Audit (DONE)
The audit confirmed:
- No global roles on User: The
identity.Usermodel has NOemployer/job_seekerfields. Roles are contextual viaMembership.role(owner/admin/member). - No product-domain models in identity layer: No
Workspace,BusinessProfile,Employee,Payroll,Project,Resumetables in the identity database. - No product-specific fields in JWT: The JWT contains only
user_id,user_status,identity_key, and optionalproduct_key/organization_id(context, not domain). - Organization is generic:
identity.Organizationhas no Bermooda-specific fields (like industry, hiring settings).
The project was already architected correctly — it was built as an independent platform rather than a product-coupled service.
Phase 2-5: Domain Separation (ALREADY DONE)
All product-specific concepts were never tightly coupled — the platform was designed with clear boundaries from the start. No destructive changes are needed.
Phase 6: JWT Payload (DONE)
JWT contains only identity claims:
- Always:
user_id,user_status,identity_key,exp,iat,iss,jti - Optional context:
product_key,organization_id(if context established at login)
No product-specific roles or permissions are embedded.
Phase 7-10: Versioning, Contracts, Security, Docs (IN PROGRESS)
- API versioning:
/v1/namespace added (parallel to/api/v1/) - Product model added to identity layer
- Active Context support in JWT
- This documentation
How a Product Integrates
Step 1: Register Product
Products register themselves with the Identity Platform:
POST /v1/products/
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"key": "bermooda",
"name": "Bermooda",
"description": "ERP platform for teams"
}
Response: { "id": "uuid", "key": "bermooda", "name": "Bermooda", ... }
Step 2: Register Application (OAuth2 Client)
Products register OAuth2 clients:
POST /v1/applications/
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"product_key": "bermooda",
"name": "Bermooda Web",
"redirect_uris": ["https://bermooda.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scopes": ["openid", "profile", "email", "org:read"]
}
Response: { "client_id": "...", "client_secret": "..." } (secret shown once)
Step 3: Authenticate Users
Products redirect users to the Identity Platform login:
https://id.example.com/login?product_key=bermooda&organization_id=<uuid>
After login, the user is redirected back with a code. The product exchanges the code for tokens:
POST /v1/auth/token
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "auth_code_from_platform",
"client_id": "bermooda-client-id",
"client_secret": "bermooda-client-secret",
"redirect_uri": "https://bermooda.example.com/callback",
"code_verifier": "..."
}
Response: { "access_token": "...", "refresh_token": "...", ... }
Step 4: Verify JWTs
Products verify access tokens using the platform's public key:
import jwt
import requests
jwks = requests.get("https://id.example.com/.well-known/jwks.json").json()
# Cache JWKS, extract public key by kid
decoded = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="bermooda-client-id",
issuer="https://id.example.com",
)
# decoded = {"user_id": "uuid", "user_status": "active", "identity_key": "usr_...", ...}
Important: The JWT sub/user_id is the SAME UUID across all products. Products use this UUID as their FK to identity.User — never creating their own user record.
Step 5: Resolve User Context
Products resolve the user's identity and context:
GET /v1/identity/
Authorization: Bearer <access_token>
Response:
{
"user": {
"id": "uuid",
"email": "user@example.com",
"phone": "+989123456789",
"status": "active",
"email_verified": true
},
"memberships": [
{ "organization": "org-uuid", "role": "admin", "status": "active" }
],
"organizations": [
{ "id": "org-uuid", "name": "ACME", "slug": "acme" }
],
"products": [
{ "key": "bermooda", "name": "Bermooda" }
]
}
The product then maps organization_id (identity) to its own concept:
- Bermooda:
Workspace.objects.get(org=org_uuid) - Hamsoo:
BusinessProfile.objects.get(user=user_uuid)
Step 6: Subscribe to Events
Products subscribe to identity events via Redis Streams:
import redis
r = redis.Redis(host="redis", port=6379, db=0)
# Create consumer group (one per product service)
r.xgroup_create("events.user", "bermooda-sync", mkstream=True)
# Consume events
while True:
events = r.xread(
{"events.user": "$"},
count=10,
block=1000,
)
for stream, messages in events:
for msg_id, msg in messages:
process_event(msg)
r.xack("events.user", "bermooda-sync", msg_id)
Backward Compatibility
The platform maintains backward compatibility:
- Old endpoints (
/api/v1/...) remain functional — they are NOT removed - New endpoints (
/v1/...) mirror the same functionality with improved naming - Old tokens remain valid until expiry
- Existing users retain their UUIDs — no data migration needed
| Old Path | New Path | Status |
|---|---|---|
/api/v1/auth/login |
/v1/auth/login |
Both work |
/api/v1/users/ |
/v1/users/ |
Both work |
/api/v1/organizations/ |
/v1/organizations/ |
Both work |
/api/v1/applications/ |
/v1/applications/ |
Both work |
/api/v1/sessions/ |
/v1/sessions/ |
Both work |
/api/v1/security/events/ |
/v1/security/events/ |
Both work |
| (NEW) | /v1/products/ |
New — product catalog |
| (NEW) | /v1/health/ |
New — health check |
Migration Checklist
- Architecture audit complete
- No product-domain models in identity layer
- No global roles on User model
- JWT contains only identity claims
- Organization is generic (no product-specific fields)
- Product model added to identity layer
- Add Active Context to admin dashboard
- Migrate product integrations to new endpoints (gradual)
- Deprecation timeline for old endpoints (6+ months)
- Full test coverage of migration scenarios
Gotchas
- Do NOT recreate users in products: Use the identity User UUID as FK
- Do NOT store passwords/tokens in products: Delegate to Identity Platform
- Do NOT embed product roles in JWT: Read from Membership API
- Do NOT hardcode product names: Use
product_keyfrom Product registration - Do NOT assume user data is complete: Products extend with their own profiles
- UUIDs are immutable: Never change a user's UUID; use
identity_keyfor external references
Support
- API Issues: Internal ticket system
- Security:
security@identity-platform.internal - Documentation:
/api/docs/(Swagger UI)