Backend — Fase 3
Admin API
11 AWS Lambda functions behind API Gateway — JWT-secured, multi-tenant, DynamoDB-powered.
Architecture
Client (Dashboard)
│
├── Authorization: Bearer <Clerk JWT>
├── x-organization-id: <org_id>
│
▼
API Gateway (eu-south-2)
│
├── OPTIONS → handle_options() [CORS preflight]
├── GET /health → health_check()
│
├── GET /users → list_users()
├── GET /users/{userId} → get_user()
│
├── GET /fingerprints → list_fingerprints()
├── GET /fingerprints/{fp} → get_fingerprint()
│
├── GET /domainprints → list_domainprints()
├── GET /domainprints/{dp} → get_domainprint()
│
├── GET /fraud/stats → get_fraud_stats()
├── GET /identity-graph → get_identity_graph()
├── GET /attribution-stats → get_attribution_stats()
└── GET /account-sharing → get_account_sharing_stats()
│
▼
DynamoDB (fingerprint-events)
├── PK: FP#{fingerprint}
├── SK: EVENT#{timestamp}
├── GSI_ORG: organizationId + timestamp
└── GSI_USER: user_id + timestampSecurity Stack
JWT RS256
Clerk JWKS verification — firma RS256 validada contra public keys
CORS Whitelist
Solo getimpress.io y localhost:3000 — Origin validado en cada request
Multi-Tenant
x-organization-id header filtra datos — zero cross-org leaks
Superadmin Bypass
role=superadmin en JWT metadata — table scan sin org filter
JWT Verification Flow
def decode_jwt_payload(token):
"""RS256 signature verification via Clerk JWKS"""
return verify_clerk_jwt(token) # jwt_verifier.py
def get_org_id_from_event(event):
# 1. Check x-organization-id header
org_id = headers.get('x-organization-id')
if org_id: return org_id
# 2. No org? Check if superadmin
jwt = decode_jwt_payload(auth_header)
if is_super_admin(jwt): # metadata.role == 'superadmin'
return None # → table.scan() (all orgs)
# 3. Not superadmin, no org → 403
raise Exception('Forbidden: No organization selected')11 Endpoints
| Lambda | Route | Description |
|---|---|---|
list-users | GET /users | List all users with event counts, risk scores, domains |
get-user | GET /users/{id} | User detail: events, fingerprints, domainprints, stats |
list-fingerprints | GET /fingerprints | List fingerprints with collision detection |
get-fingerprint | GET /fingerprints/{fp} | Fingerprint detail: events, user associations |
list-domainprints | GET /domainprints | List domainprints with cross-domain stats |
get-domainprint | GET /domainprints/{dp} | Domainprint detail: users, fingerprints, domains |
get-fraud-stats | GET /fraud/stats | Fraud dashboard: risk levels, signals, impossible travel |
get-identity-graph | GET /identity-graph | Cross-domain identity relationships |
attribution-stats | GET /attribution | Campaign attribution aggregated stats |
account-sharing | GET /account-sharing | Account sharing detection stats |
get-health | GET /health | Service health + DynamoDB ping |
Common Patterns
DecimalEncoder
All 11 Lambdas share a custom JSON encoder for DynamoDB Decimal types:
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return int(obj) if obj % 1 == 0 else float(obj)
return super().default(obj)CORS Headers
Origin-validated CORS with Vary: Origin:
ALLOWED_ORIGINS = [
'https://getimpress.io',
'https://www.getimpress.io',
'http://localhost:3000',
]
def cors_headers(event=None):
origin = headers.get('origin')
allowed = origin if origin in ALLOWED_ORIGINS
else ALLOWED_ORIGINS[0]
return {
'Access-Control-Allow-Origin': allowed,
'Vary': 'Origin'
}Multi-Tenant Query Pattern
Every endpoint isolates data by organization:
if org_id:
# Normal user → GSI_ORG query (fast, isolated)
response = table.query(
IndexName='GSI_ORG',
KeyConditionExpression='organizationId = :orgId',
ExpressionAttributeValues={':orgId': org_id}
)
else:
# Superadmin → full table scan (all orgs)
response = table.scan(Limit=5000)Deep Dive: Endpoint Reference
Request/response schemas, query parameters, and GSI usage for each endpoint.
View Endpoint Reference