8.3 KiB
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
Sessionrecord (type: browser/api/device) - Refresh token stored hashed; rotatable; revocable
- Device fingerprinting for detection
Organization Model
- Single
Organizationmodel reusable as company/team/workspace Membershiplinks 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
SecurityEventmodel - 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/<slug>/ |
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/<token>/ |
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)
services:
postgres: postgres:16-alpine
redis: redis:7-alpine
api: gunicorn (3 workers, serves API + account pages)
Environment Separation
.envfor secrets (not committed).env.exampleas templateDJANGO_ENV=productionenforcesDJANGO_SECRET_KEY
Static Files
- Django: WhiteNoise (
CompressedManifestStaticFilesStorage) - Account pages carry their CSS inline;
collectstaticis 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