The User Service is a FastAPI (Python) microservice that owns user profile data, orchestrates KYC (Know Your Customer) verification workflows, manages consent/preferences, and maintains an audit trail of all user-facing changes.
- Profile & Identity: Manage user profile data (name, DOB, address, PAN, Aadhaar) with field-level encryption for PII.
- KYC Orchestration: Integrate with third-party KYC vendors (HyperVerge, Signzy) for identity verification, liveness checks, and document validation.
- Account State Management: Track user lifecycle states (
active, suspended, frozen, closed) with transitions triggered by KYC outcomes or admin actions.
- Consent & Preferences: Track user consents (terms, privacy, marketing) and notification preferences.
- Audit Trail: Log all profile changes and KYC actions to an append-only audit log (ClickHouse for high-volume, PostgreSQL for transactional).
- Event Producer: Emit
user.created, user.updated, user.verified, user.frozen, user.kyc.* events.
User Service does not handle authentication (that’s Auth Service) or financial operations (that’s Wallet Service).
graph TB
Client[Client App] -->|REST| UserAPI[User Service API]
AuthSvc[Auth Service] -->|auth.created.v1| Kafka1[Kafka]
Kafka1 --> UserSvc[User Service]
UserSvc -->|Stores| PG[(PostgreSQL)]
UserSvc -->|PII Encrypted| PG
UserSvc -->|Audit| CH[(ClickHouse)]
UserSvc -->|Events| Kafka2[Kafka]
UserSvc -->|KYC| HV[HyperVerge]
UserSvc -->|KYC| Signzy[Signzy]
Kafka2 --> WalletSvc[Wallet Service]
Kafka2 --> RiskSvc[Risk Service]
| Field | Type | Description |
|---|
id | UUID (PK) | User ID (maps 1 to auth_accounts.id) |
auth_id | UUID UNIQUE | FK to auth service identity |
status | ENUM(pending, active, suspended, frozen, closed) | User lifecycle state |
kyc_status | ENUM(not_started, pending, in_review, approved, rejected, expired) | KYC verification state |
kyc_tier | ENUM(tier_0, tier_1, tier_2, tier_3) | Verification level (affects limits) |
created_at | TIMESTAMPTZ | Account creation time |
updated_at | TIMESTAMPTZ | Last modification time |
verified_at | TIMESTAMPTZ | When KYC was approved |
| Field | Type | Description |
|---|
id | UUID (PK) | Profile row ID |
user_id | UUID (FK) | References users |
first_name_enc | BYTEA | Encrypted first name |
last_name_enc | BYTEA | Encrypted last name |
date_of_birth_enc | BYTEA | Encrypted date of birth |
pan_number_enc | BYTEA | Encrypted PAN (Indian tax ID) |
aadhaar_last4_enc | BYTEA | Encrypted last 4 digits of Aadhaar |
address_enc | BYTEA | Encrypted address JSON |
phone_enc | BYTEA | Encrypted phone number |
email | CITEXT | Email (not encrypted, used for lookup) |
avatar_url | TEXT | Profile picture URL |
updated_at | TIMESTAMPTZ | Last modification time |
Encryption: Field-level encryption using pgcrypto or application-level AES-256-GCM with KMS-managed keys.
| Field | Type | Description |
|---|
id | UUID (PK) | KYC case ID |
user_id | UUID (FK) | References users |
vendor | ENUM(hyperverge, signzy) | KYC vendor |
vendor_case_id | TEXT | Vendor’s internal case reference |
status | ENUM(initiated, documents_submitted, in_review, approved, rejected, expired) | Case status |
rejection_reason | TEXT | Reason for rejection (if applicable) |
initiated_at | TIMESTAMPTZ | When KYC was started |
completed_at | TIMESTAMPTZ | When vendor returned result |
expires_at | TIMESTAMPTZ | When KYC result expires |
| Field | Type | Description |
|---|
id | UUID (PK) | Artifact ID |
kyc_case_id | UUID (FK) | References kyc_cases |
artifact_type | ENUM(selfie, pan_card, aadhaar_front, aadhaar_back, address_proof, liveness_video) | Document type |
storage_ref | TEXT | Encrypted reference to stored artifact (S3/GCS) |
hash | TEXT | SHA-256 hash of the artifact |
uploaded_at | TIMESTAMPTZ | Upload timestamp |
| Field | Type | Description |
|---|
id | UUID (PK) | Consent record ID |
user_id | UUID (FK) | References users |
consent_type | ENUM(terms_of_service, privacy_policy, marketing, data_sharing, risk_disclosure) | Type of consent |
version | TEXT | Version of the document consented to |
granted | BOOLEAN | Whether consent was granted |
ip_addr | INET | IP from which consent was given |
granted_at | TIMESTAMPTZ | When consent was granted |
revoked_at | TIMESTAMPTZ | When consent was revoked (NULL if active) |
| Field | Type | Description |
|---|
user_id | UUID (PK, FK) | References users |
email_trade_fills | BOOLEAN | Email on trade fills |
email_deposits | BOOLEAN | Email on deposits |
email_withdrawals | BOOLEAN | Email on withdrawals |
email_security_alerts | BOOLEAN | Email on security events |
push_enabled | BOOLEAN | Push notifications enabled |
sms_enabled | BOOLEAN | SMS notifications enabled |
updated_at | TIMESTAMPTZ | Last update |
| Field | Type | Description |
|---|
event_id | UUID | Unique event ID |
user_id | UUID | Subject user |
actor_id | UUID | User or admin who performed action |
action | String | profile.updated, kyc.initiated, consent.granted, status.changed, etc. |
resource | String | Resource affected |
old_value | String | Previous value (JSON) |
new_value | String | New value (JSON) |
ip_addr | String | Client IP |
timestamp | DateTime | Event timestamp |
| Key Pattern | Purpose | TTL |
|---|
user:profile:{user_id} | Cached profile (decrypted, for authorized internal use) | 5 min |
user:kyc:status:{user_id} | Cached KYC status | 5 min |
user:rate:{user_id} | Profile update rate limiter | Sliding window |
| Method | Endpoint | Description |
|---|
GET | /v1/me | Get own profile |
PATCH | /v1/me | Update profile fields |
POST | /v1/me/consents | Grant or revoke consents |
GET | /v1/me/consents | Get consent status |
PUT | /v1/me/notification-prefs | Update notification preferences |
GET | /v1/me/notification-prefs | Get notification preferences |
| Method | Endpoint | Description |
|---|
POST | /v1/kyc/initiate | Start KYC verification flow |
GET | /v1/kyc/status | Get current KYC status |
POST | /v1/kyc/resubmit | Resubmit after rejection |
POST | /v1/kyc/webhook | Vendor callback (HyperVerge/Signzy) |
| Method | Endpoint | Description |
|---|
GET | /internal/users/{id} | Get user details (internal) |
POST | /internal/users/{id}/freeze | Freeze user account |
POST | /internal/users/{id}/unfreeze | Unfreeze user account |
POST | /internal/users/{id}/close | Close user account |
GET | /internal/users/{id}/audit | Get audit trail |
POST | /admin/users/search | Search users (admin) |
POST | /admin/kyc/manual-review | Manual KYC decision (admin) |
| Topic | Description |
|---|
auth.created.v1 | New auth account → create user record |
auth.account.deactivated.v1 | Auth suspension → freeze user |
| Topic | Description |
|---|
user.created.v1 | New user record created |
user.updated.v1 | Profile updated |
user.verified.v1 | KYC approved, user verified |
user.frozen.v1 | User account frozen |
user.unfrozen.v1 | User account unfrozen |
user.kyc.initiated.v1 | KYC flow started |
user.kyc.updated.v1 | KYC status changed |
- Consume
auth.created.v1 event
- Create
users row (status = pending, kyc_status = not_started)
- Create empty
user_profile row
- Create default
notification_prefs
- Emit
user.created.v1
- Wallet Service consumes
user.created.v1 and creates initial balance records
- User calls
POST /v1/kyc/initiate
- Service creates
kyc_cases row, selects vendor based on configuration
- Service calls vendor API (HyperVerge/Signzy) to initiate verification
- Return redirect URL or SDK token to client
- User completes verification on vendor UI (selfie, PAN, Aadhaar)
- Vendor sends webhook to
POST /v1/kyc/webhook
- Service processes webhook:
- Extract verification results
- Store
kyc_artifacts references
- Update
kyc_cases.status
- If approved: set
users.kyc_status = approved, users.kyc_tier = tier_1
- If rejected: set
users.kyc_status = rejected with reason
- Emit
user.kyc.updated.v1
- If approved: emit
user.verified.v1 → other services update limits
- Admin or Risk Service calls
POST /internal/users/{id}/freeze
- Set
users.status = frozen
- Emit
user.frozen.v1
- Downstream effects: Wallet holds funds, Order Service rejects new orders
- Purpose: Liveness detection, face match, PAN verification
- Flow: SDK-based (mobile/web) → webhook callback
- Documents: Selfie, PAN card image
- Purpose: Aadhaar verification, address proof, bank account verification
- Flow: API-based verification → webhook callback
- Documents: Aadhaar (DigiLocker), bank statement
| Tier | Requirements | Limits |
|---|
| Tier 0 | Email verified only | View-only, no trading |
| Tier 1 | PAN + Selfie verified | Basic trading limits |
| Tier 2 | Aadhaar + Address verified | Enhanced limits |
| Tier 3 | Full verification + bank | Maximum limits |
All personally identifiable information is encrypted at the field level:
- Algorithm: AES-256-GCM (via
pgcrypto or application-level)
- Key Management: Keys managed via KMS (AWS KMS / HashiCorp Vault)
- Searchable Fields: Email is stored in cleartext CITEXT for lookup; all other PII is encrypted
- Decryption: Only User Service has decryption keys; other services receive masked data
- Audit: Every decryption is logged to
user_audit
user_created_total — User records created
user_kyc_initiated_total{vendor} — KYC flows started by vendor
user_kyc_completed_total{vendor,status} — KYC completions by outcome
user_profile_update_total — Profile updates
user_frozen_total{reason} — Accounts frozen
Spans: user.create, user.update_profile, user.initiate_kyc, user.process_kyc_webhook, user.freeze, user.unfreeze
Structured JSON: user_id, action, kyc_case_id, vendor, status, trace_id. PII is never logged in cleartext.
- PII Encryption: AES-256-GCM field-level encryption for all personal data
- Webhook Verification: Vendor webhooks verified via HMAC signature
- Rate Limiting: Profile updates limited to prevent abuse
- Consent Audit: Every consent grant/revoke recorded with IP and timestamp
- Admin Authorization: Admin endpoints require elevated JWT scope
| Scenario | Handling |
|---|
| KYC vendor API timeout | Return pending status; background job retries |
| Webhook delivery failure | Vendor retries; idempotent processing by vendor_case_id |
| PII decryption failure | Return masked data; alert ops for KMS key issue |
| Auth event missed | Eventual consistency: user creation can be triggered on first API call |
| Metric | Target |
|---|
| Profile read latency (p95) | < 50 ms |
| KYC initiation latency (p95) | < 2 seconds |
| Webhook processing latency (p95) | < 500 ms |
| PII encryption/decryption | < 5 ms per field |
| Audit log write success | >= 99.9% |
| Variable | Description | Default |
|---|
POSTGRES_URL | PostgreSQL connection string | Required |
CLICKHOUSE_URL | ClickHouse for audit logs | Required |
REDIS_URL | Redis connection string | Required |
KAFKA_BROKERS | Kafka broker addresses | Required |
HYPERVERGE_API_KEY | HyperVerge API key | Required (prod) |
SIGNZY_API_KEY | Signzy API key | Required (prod) |
PII_ENCRYPTION_KEY_ID | KMS key ID for PII encryption | Required |
- Language: Python 3.10+
- Framework: FastAPI + Uvicorn
- ORM: SQLAlchemy + Alembic
- Kafka: confluent-kafka-python with Avro
- Encryption: pgcrypto / AES-256-GCM
- Package Manager: uv