# Architecture ## Overview Identity Platform follows an **Adobe-style holding model**: the Platform is the central hub/reference, and products (Bermooda, Hamsoo, future apps) are sub-products that connect to it. The Platform owns identity; products own their domain data. ``` ┌──────────────────────────────────────────────────────────────┐ │ IDENTITY PLATFORM │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ Identity │ │ Authentication│ │ Organization │ │ │ │ (User) │ │ (Session) │ │ (Membership)│ │ │ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │ │ └────────────────┴────────────────┘ │ │ PRIVATE KEY (RS256) — NEVER LEAVES PLATFORM │ └────────────────────────────┬─────────────────────────────────┘ │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ Bermooda │ │ Hamsoo │ │ Future │ │ (ERP) │ │ (Network) │ │ Apps │ └───────────┘ └───────────┘ └───────────┘ ▲ ▲ ▲ │ PUBLIC KEY (verify only) │ └──────────────┴──────────────┘ ``` ## Data Ownership Boundary | Data | Owner | Notes | |------|-------|-------| | **User ID (UUID)** | Identity Platform | Immutable, globally unique | | **Email & Phone** | Identity Platform | Verified, used for auth | | **Password Hash** | Identity Platform | Never exposed | | **Authentication & Sessions** | Identity Platform | Full lifecycle | | **Organizations** | Identity Platform | Shared across products | | **Memberships & Roles** | Identity Platform | Owner/Admin/Member | | **Applications (OAuth Clients)** | Identity Platform | Per-product credentials | | **Service Credentials** | Identity Platform | Client credentials flow | | **Security Events / Audit Log** | Identity Platform | Immutable, queryable | | **Employee & Payroll** | Bermooda | Domain data | | **Profile & Resume** | Hamsoo | Domain data | | **Projects & Listings** | Product | Domain data | **Rule:** Identity Platform never stores domain data (payroll, resumes, projects). Products never store auth data (passwords, sessions, tokens). ## Modular Monolith (Backend) The Django backend is a **modular monolith** with 11 apps: | App | Responsibility | |-----|----------------| | `common` | Base models, permissions, pagination, rate limiting, exceptions | | `identity` | Custom User model (UUID PK), managers, serializers | | `authentication` | Login, refresh, logout, MFA-ready, rate limiting | | `organization` | Organization CRUD, slug generation | | `membership` | Membership model (User↔Org), roles, statuses | | `application` | OAuth clients (confidential/public), scopes | | `session` | Session tracking, device info, revocation | | `security` | SecurityEvent model, audit log | | `oauth` | OIDC discovery, JWKS, authorization server stubs | | `config` | Settings, URLs, WSGI/ASGI | | `admin` | Django admin registrations | ## API Versioning All endpoints under `/api/v1/`. Breaking changes → `/api/v2/`. OpenAPI schema via `drf-spectacular` at: - JSON: `/api/schema/` - Swagger UI: `/api/docs/` - ReDoc: `/api/redoc/` ## Key Design Decisions ### UUID Primary Keys - User PK = UUID (immutable, unguessable) - All references use UUID — no integer IDs exposed ### RS256 Asymmetric Signing - Platform holds **private key** (signing tokens) - Products hold **public key** (verifying tokens via JWKS) - Private key **never** leaves platform — products cannot forge tokens ### Session Model - Each login creates a `Session` record (type: browser/api/device) - Refresh token stored hashed; rotatable; revocable - Device fingerprinting for detection ### Organization Model - Single `Organization` model reusable as company/team/workspace - `Membership` links User→Org with role (owner/admin/member) and status - Products query memberships for authorization ### Rate Limiting - Per-user and per-IP via Django cache (Redis in prod) - Configurable windows; brute-force triggers lockout - Endpoint-level decorators ### Security Events - Immutable `SecurityEvent` model - Types: `login_success`, `login_failed`, `logout`, `token_refreshed`, `session_revoked`, `password_changed`, `mfa_changed`, `suspicious_activity` - Severity: `low`, `medium`, `high`, `critical` ## Web UI (server-rendered) The account UI is rendered by Django templates — no Node.js toolchain, no separate frontend service, no build step. | Route | View | Purpose | |-------|------|---------| | `/` | `identity.ui_views.home` | Landing page | | `/register/` | `identity.ui_views.register` | Self-registration (reuses `RegisterSerializer`) | | `/account/` | `identity.ui_views.account` | Profile summary and active sessions | | `/account/profile/` | `identity.ui_views.profile` | Edit self-service profile fields | | `/account/security/` | `identity.ui_views.security` | Email verification, TOTP MFA, recovery codes, passkeys | | `/account/organizations/` | `identity.ui_views.organizations` | Workspaces the user belongs to + create | | `/account/organizations//` | `identity.ui_views.organization_detail` | Members and roles (owners/admins only) | | `/account/developers/` | `identity.ui_views.developers` | OAuth clients; staff can register apps and rotate secrets | | `/account/password/` | `identity.ui_views.password_change` | Change password | | `/account/reset-password/` | `identity.ui_views.password_reset` | Request and confirm reset (`?token=`) | | `/p//` | `passport.ui_views.public_passport` | Public share-link passport page | | `/sso/login/` | `sso.views.SsoLoginView` | Central login (session cookie for `*.hamsoo.me`) | | `/oauth/consent` | `oauth.views.ConsentView` | First-time authorization consent | | `/admin/` | Django admin | Users, organizations, applications, sessions, events | Templates live next to their apps (`apps/*/templates/`) and share `identity/templates/base.html`. Styling is self-contained inline CSS in that base template, so WhiteNoise/collectstatic is not on the critical path for the account pages. ### Why no SPA The platform is an IdP: its pages are credential entry points. Server rendering keeps passwords and tokens out of client-side state, gives CSRF protection by default, and removes a second deployable. Products (Hamsoo, Bermooda) keep their own frontends and talk to the API/OIDC endpoints. ## Deployment ### Docker Compose (Production-Ready) ```yaml services: postgres: postgres:16-alpine redis: redis:7-alpine api: gunicorn (3 workers, serves API + account pages) ``` ### Environment Separation - `.env` for secrets (not committed) - `.env.example` as template - `DJANGO_ENV=production` enforces `DJANGO_SECRET_KEY` ### Static Files - Django: WhiteNoise (`CompressedManifestStaticFilesStorage`) - Account pages carry their CSS inline; `collectstatic` is only needed for the Django admin assets. ## Scaling Considerations | Component | Horizontal Scale | |-----------|------------------| | API (Django) | Stateless — add workers/containers behind LB | | PostgreSQL | Read replicas for query-heavy workloads | | Redis | Cluster mode for cache/sessions | | Web UI (Django) | Horizontal scale with the API workers | ## Future: Multi-Region - Platform deployable per region with shared user namespace - Cross-region session sync via Redis - DNS-based routing to nearest region