Discord/Web → API Gateway → Proxy Service → Pub/Sub Topics → Processor Services → Webhooks (Discord/Web)
Note: Microservices architecture with separation of concerns and specialized services.
┌─────────────────────────────────────────────────────────────────────────────┐
│ DISCORD BOT INTERACTION │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ API GATEWAY (GCP) │
│ • Single entry point │
│ • URL: https://guidon-*.ew.gateway.dev │
│ • Endpoints: │
│ - POST /discord/interactions (Discord bot) │
│ - POST /web/interactions (Web clients) │
│ - GET /health │
│ - GET /auth/* (Auth service) │
│ - GET /* (Web frontend) │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROXY SERVICE (Cloud Functions Gen2) │
│ Service: proxy │
│ Authentication: Private (IAM - API Gateway only) │
│ Secrets: DISCORD_PUBLIC_KEY, DISCORD_BOT_TOKEN, DISCORD_APPLICATION_ID │
│ │
│ Steps: │
│ 1. Verify Discord signature (DISCORD_PUBLIC_KEY) │
│ 2. Parse interaction │
│ 3. Handle simple commands directly (ping, hello) │
│ 4. For complex commands → Publish to Pub/Sub │
│ 5. Respond immediately to Discord (type 5 - deferred) │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌─────────▼─────────┐ ┌─────────▼──────────┐
│ SIMPLE COMMANDS │ │ COMPLEX COMMANDS │
│ (ping, hello) │ │ (help, draw, etc) │
│ │ │ │
│ Direct response │ │ Type 5 (deferred) │
│ Type 4 │ │ + Pub/Sub │
└───────────────────┘ └────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PUB/SUB TOPICS (GCP) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ interactions │ │commands-base │ │commands-draw │ │commands- │ │
│ │ │ │ │ │ │ │ snapshot │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │commands- │ │commands-stats│ │commands- │ │commands- │ │
│ │canvas-state │ │ │ │colors │ │pixel-info │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ All topics trigger Cloud Functions Gen2 via Eventarc │
└─────────────────────────────────────────────────────────────────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ PROCESSOR-BASE │ │ PROCESSOR-DRAW │ │ PROCESSOR-* │
│ (Cloud Functions) │ │ (Cloud Functions) │ │ (Cloud Functions) │
│ │ │ │ │ │
│ Steps: │ │ Steps: │ │ Steps: │
│ 1. Receive message │ │ 1. Receive message │ │ 1. Receive message │
│ 2. Process command │ │ 2. Check rate limit │ │ 2. Process command │
│ 3. Send to Discord │ │ (user-manager) │ │ 3. Call canvas- │
│ via webhook │ │ 3. Draw pixel │ │ service │
│ (direct) │ │ (canvas-service) │ │ 4. Send to Discord/ │
│ │ │ 4. Increment usage │ │ Web via webhook │
│ │ │ (user-manager) │ │ (direct) │
│ │ │ 5. Send to Discord │ │ │
│ │ │ via webhook │ │ │
│ │ │ (direct) │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │
│ │ │
└────────────────────┴────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ DISCORD │
│ User receives bot response (via webhook) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ WEB CLIENT (Browser) │
│ User authenticated via Discord OAuth2 │
│ Session ID stored in localStorage │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ POST /web/interactions
│ Headers: X-Session-ID: <session_id>
│ Body: { "command": "draw", "x": 10, ... }
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ API GATEWAY (GCP) │
│ Endpoint: POST /web/interactions │
│ Routes to: proxy service │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ (IAM authenticated)
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROXY SERVICE (Cloud Functions Gen2) │
│ Service: proxy │
│ Authentication: Private (IAM - API Gateway only) │
│ │
│ Steps: │
│ 1. Extract session ID from X-Session-ID header or Authorization Bearer │
│ 2. Call auth-service POST /auth/verify with session ID │
│ 3. Receive verified Discord user info (user_id, username, avatar) │
│ 4. Inject user info into interaction payload │
│ 5. Parse JSON request (command, options, webhook_url) │
│ 6. Route to appropriate handler │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌─────────▼─────────┐ ┌─────────▼──────────┐
│ SIMPLE COMMANDS │ │ COMPLEX COMMANDS │
│ (ping, hello, │ │ (draw, snapshot, │
│ help) │ │ stats, etc) │
│ │ │ │
│ Process directly │ │ Publish to Pub/Sub│
│ Return JSON │ │ Return 202 │
│ (200 OK) │ │ Accepted │
└───────────────────┘ └────────────────────┘
│
│ Pub/Sub message
│ { command, user, webhook_url }
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PUB/SUB TOPICS (GCP) │
│ Topic routing based on command: │
│ • commands-draw → processor-draw │
│ • commands-snapshot → processor-snapshot │
│ • commands-stats → processor-stats │
│ • commands-canvas-state → processor-canvas-state │
│ • commands-colors → processor-colors │
│ • commands-pixel-info → processor-pixel-info │
│ • commands-base → processor-base │
│ │
│ All topics trigger Cloud Functions Gen2 via Eventarc │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ Eventarc trigger
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROCESSOR SERVICE (Cloud Functions Gen2) │
│ Example: processor-draw │
│ │
│ Steps: │
│ 1. Receive Pub/Sub message (via Eventarc) │
│ 2. Extract command, user info, webhook_url from message │
│ 3. Check rate limit (user-manager via identity token) │
│ 4. Execute command (e.g., draw pixel via canvas-service) │
│ 5. Increment user usage (user-manager via identity token) │
│ 6. Generate response payload │
│ 7. Send HTTP POST to webhook_url provided by client │
│ (webhook_url = https://web-frontend-url/webhook) │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ HTTP POST to webhook_url
│ Body: { "token": "...", "status": "success", "data": {...} }
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ WEB FRONTEND (App Engine) │
│ POST /webhook: Receives webhook response from processor │
│ Stores response in memory with token as key │
│ GET /response/<token>: Returns stored response (polling endpoint) │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ Client polls GET /response/<token>
│ (every 500ms-1000ms, max 30 attempts)
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ WEB CLIENT │
│ Polls GET /response/<token> until response is available │
│ Updates UI with response data when received │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA SERVICES │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ CANVAS-SERVICE │ │ USER-MANAGER │ │
│ │ (Cloud Functions Gen2) │ │ (Cloud Functions Gen2) │ │
│ │ Authentication: Private │ │ Authentication: Private │ │
│ │ (Identity Tokens) │ │ (Identity Tokens) │ │
│ │ │ │ │ │
│ │ REST API: │ │ REST API: │ │
│ │ • POST /canvas/draw │ │ • GET /api/users/{id} │ │
│ │ • GET /canvas/state │ │ • POST /api/users/{id}/ │ │
│ │ • POST /canvas/snapshot │ │ increment │ │
│ │ • GET /canvas/stats │ │ • POST /api/rate-limit/ │ │
│ │ • GET /canvas/pixel/{x}/{y}│ │ check │ │
│ │ │ │ • GET /api/stats/ │ │
│ │ Storage: │ │ leaderboard │ │
│ │ • Firestore (canvas state) │ │ │ │
│ │ • GCS (snapshots) │ │ Storage: │ │
│ │ │ │ • Firestore (users) │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ AUTH-SERVICE │ │
│ │ (Cloud Functions Gen2) │ │
│ │ Authentication: Public │ │
│ │ │ │
│ │ REST API: │ │
│ │ • GET /auth/login │ │
│ │ • GET /auth/callback │ │
│ │ • POST /auth/verify │ │
│ │ • GET /auth/user │ │
│ │ • POST /auth/logout │ │
│ │ │ │
│ │ Storage: │ │
│ │ • Firestore (sessions) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
│ │ │
└────────────────────┴────────────────────┘
│
│ (Identity Token Auth)
│
┌───────────────┴───────────────┐
│ │
┌───────▼────────┐ ┌─────────▼────────┐
│ PROCESSORS │ │ PROXY │
│ (via shared │ │ (for web │
│ clients) │ │ auth) │
└────────────────┘ └──────────────────┘
Discord → API Gateway → Proxy
│
├─► Verify signature
├─► Process directly
└─► Respond to Discord (Type 4)
Web Client → API Gateway → Proxy
│
├─► Extract X-Session-ID header or Authorization Bearer token
├─► Call auth-service POST /auth/verify with session ID
├─► Receive verified user info (user_id, username, avatar)
├─► Inject user info into interaction payload
├─► Parse JSON request (command, options, webhook_url)
├─► Process command directly
└─► Return JSON response (200 OK)
Total time: < 1 second
Discord → API Gateway → Proxy
│
├─► Verify signature
├─► Publish to Pub/Sub
└─► Respond to Discord (Type 5 - deferred)
│
▼
Pub/Sub Topic (commands-draw)
│
▼ (Eventarc trigger)
Processor-Draw (Cloud Functions Gen2)
│
├─► Check rate limit (user-manager via identity token)
├─► Draw pixel (canvas-service via identity token)
├─► Increment usage (user-manager via identity token)
├─► Generate response
└─► Send directly to Discord via webhook
│
▼
Discord displays response
Web Client → API Gateway → Proxy
│
├─► Extract X-Session-ID header
├─► Call auth-service POST /auth/verify
├─► Receive verified user info
├─► Inject user info into payload
├─► Extract webhook_url from request
├─► Publish to Pub/Sub with webhook_url
└─► Return 202 Accepted
│
▼
Pub/Sub Topic (commands-draw)
│
▼ (Eventarc trigger)
Processor-Draw (Cloud Functions Gen2)
│
├─► Check rate limit (user-manager)
├─► Draw pixel (canvas-service)
├─► Increment usage (user-manager)
├─► Generate response payload
└─► POST to webhook_url provided by client
│
▼
Web client receives response at webhook_url
Total time:
- Discord: Initial response < 3 seconds (Type 5), Final response up to 15 minutes (via webhook)
- Web: Initial response < 3 seconds (202 Accepted), Final response via webhook callback
- Role: Single entry point, traffic management
- URL:
https://guidon-*.ew.gateway.dev - Endpoints:
POST /discord/interactions→ Proxy (Discord bot)POST /web/interactions→ Proxy (Web clients, requiresX-Session-IDheader)GET /health→ ProxyGET /auth/*→ Auth ServiceGET /*→ Web Frontend
- Authentication: IAM - Only API Gateway service account can invoke proxy
- Service:
proxy - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: API Gateway service account only
- Secrets:
DISCORD_PUBLIC_KEY,DISCORD_BOT_TOKEN,DISCORD_APPLICATION_ID - Responsibilities:
- Discord signature verification (for Discord interactions)
- Web session authentication (calls auth-service /auth/verify)
- Simple command handling (ping, hello, help) - both Discord and Web
- Publishing to Pub/Sub for complex commands
- No longer handles processor responses (processors send directly via webhooks)
- interactions: General interactions
- commands-base: Base commands (hello, ping, help)
- commands-draw: Draw command
- commands-snapshot: Snapshot command
- commands-canvas-state: Canvas state command
- commands-stats: Stats command
- commands-colors: Colors command
- commands-pixel-info: Pixel info command
All processors are Cloud Functions Gen2 triggered by Pub/Sub via Eventarc.
- Service:
processor-base - Topic:
commands-base - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Eventarc service account (automatic via Pub/Sub trigger)
- Secrets:
DISCORD_BOT_TOKEN(for Discord webhooks) - Responsibilities:
- Process base commands (ping, hello, help)
- Send responses directly to Discord/web via webhooks
- Service:
processor-draw - Topic:
commands-draw - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Eventarc service account (automatic via Pub/Sub trigger)
- Secrets:
DISCORD_BOT_TOKEN - Dependencies:
canvas-service(REST API via identity token)user-manager(REST API via identity token)
- Responsibilities:
- Check rate limits
- Draw pixels on canvas
- Increment user usage
- Send responses directly to Discord/web via webhooks
- Service:
processor-snapshot - Topic:
commands-snapshot - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Eventarc service account (automatic via Pub/Sub trigger)
- Secrets:
DISCORD_BOT_TOKEN - Dependencies:
canvas-service(REST API via identity token)user-manager(REST API via identity token)
- Responsibilities:
- Create canvas snapshots
- Send responses directly to Discord/web via webhooks
- Service:
processor-canvas-state - Topic:
commands-canvas-state - Authentication: Public (
--allow-unauthenticated) - Secrets:
DISCORD_BOT_TOKEN - Dependencies:
canvas-service(REST API via identity token)
- Responsibilities:
- Get canvas state as JSON
- Send responses directly to Discord/web via webhooks
- Service:
processor-stats - Topic:
commands-stats - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Eventarc service account (automatic via Pub/Sub trigger)
- Secrets:
DISCORD_BOT_TOKEN - Dependencies:
canvas-service(REST API via identity token)user-manager(REST API via identity token)
- Responsibilities:
- Get canvas and user statistics
- Send responses directly to Discord/web via webhooks
- Service:
processor-colors - Topic:
commands-colors - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Eventarc service account (automatic via Pub/Sub trigger)
- Secrets:
DISCORD_BOT_TOKEN - Dependencies: None (stateless)
- Responsibilities:
- List available colors
- Send responses directly to Discord/web via webhooks
- Service:
processor-pixel-info - Topic:
commands-pixel-info - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Eventarc service account (automatic via Pub/Sub trigger)
- Secrets:
DISCORD_BOT_TOKEN - Dependencies:
canvas-service(REST API via identity token)
- Responsibilities:
- Get pixel information
- Send responses directly to Discord/web via webhooks
- Service:
canvas-service - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Identity tokens (service-to-service)
- Role: Data layer for canvas operations
- Storage: Firestore (canvas state), GCS (snapshots)
- Configuration:
setting.json - Environment Variables:
GCS_CANVAS_BUCKET(from Secret Manager) - GCS bucket name for snapshotsUSER_MANAGER_URL(from Secret Manager) - User manager service URLFIRESTORE_DATABASE- Firestore database IDGCP_PROJECT_ID- GCP project IDENVIRONMENT- Environment name (dev/stage/prod)
- Service:
user-manager - Authentication: Private (
--no-allow-unauthenticated- IAM only) - IAM Access: Identity tokens (service-to-service)
- Role: User management, rate limiting, statistics
- Storage: Firestore (users collection)
- Service:
discord-auth-service - Authentication: Public (
--allow-unauthenticated) - Role: OAuth2 authentication for web clients
- Storage: Firestore (sessions collection)
- Service:
web-frontend - Authentication: Public (
--allow-unauthenticated) - Role: Web interface for canvas
- Pages: Login, Canvas, Session (legacy)
- Endpoints:
GET /- Canvas pageGET /login- Login pagePOST /webhook- Receives webhook responses from processorsGET /response/<token>- Returns stored response for polling
- Response Handling: Hybrid webhook + polling system
- Processors send responses to
/webhookendpoint - Frontend stores responses in memory with token as key
- Client polls
/response/<token>every 500ms-1000ms until response is available
- Processors send responses to
- Environment Variables:
GATEWAY_URL- API Gateway URLAUTH_SERVICE_URL- Auth service URLWEB_FRONTEND_URL- Self URL (dynamically built if not set)
- Service:
discord-utils - Authentication: Public (
--allow-unauthenticated) - Secrets:
DISCORD_BOT_TOKEN,DISCORD_APPLICATION_ID - Role: Register Discord slash commands
┌─────────────────────────────────────────────────────────┐
│ AUTHENTICATION │
│ │
│ Public Services (--allow-unauthenticated): │
│ • web-frontend (public access) │
│ • auth-service (public access) │
│ • discord-registrar (public access) │
│ • processor-canvas-state (public access) │
│ │
│ Private Services (--no-allow-unauthenticated): │
│ • proxy (IAM - API Gateway service account only) │
│ • user-manager (IAM - identity tokens) │
│ • canvas-service (IAM - identity tokens) │
│ • processor-base (IAM - Eventarc service account) │
│ • processor-draw (IAM - Eventarc service account) │
│ • processor-snapshot (IAM - Eventarc service account) │
│ • processor-stats (IAM - Eventarc service account) │
│ • processor-colors (IAM - Eventarc service account) │
│ • processor-pixel-info (IAM - Eventarc service account)│
└─────────────────────────────────────────────────────────┘
- Identity Tokens: Services use Google Cloud identity tokens for authentication
- Shared Clients:
shared/canvas_client.py- Client for canvas-service (identity token auth)shared/user_client.py- Client for user-manager (identity token auth)
- Eventarc: Automatically creates Pub/Sub subscriptions for Cloud Functions Gen2
- IAM Permissions: Eventarc service account automatically receives
roles/run.invokerpermission - Private Processors: Even though processors are private (
--no-allow-unauthenticated), Eventarc can invoke them because:- Eventarc service account has
roles/run.invokeron the function - This is automatically configured when deploying with
--trigger-topic
- Eventarc service account has
- No Manual Subscription Creation: Cloud Functions Gen2 + Eventarc handles subscription creation automatically
- OAuth2 Flow: Web clients authenticate via Discord OAuth2 (auth-service)
- Session Tokens: Validated by proxy before processing web interactions
- Session Storage: Firestore (sessions collection)
┌─────────────────────────────────────────────────────────┐
│ SECRET MANAGER (GCP) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ DISCORD_PUBLIC_ │ │ DISCORD_BOT_ │ │
│ │ KEY │ │ TOKEN │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ DISCORD_ │ │ DISCORD_CLIENT_ │ │
│ │ APPLICATION_ID │ │ ID/SECRET │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ GCS_CANVAS_ │ │ USER_MANAGER_ │ │
│ │ BUCKET │ │ URL │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ CANVAS_SERVICE_ │ │ AUTH_SERVICE_ │ │
│ │ URL │ │ URL │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ PROXY │ │ REGISTRAR │ │ PROCESSORS │
│ All secrets │ │ BOT_TOKEN │ │ BOT_TOKEN │
│ │ │ APP_ID │ │ (webhooks) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ CANVAS-SERVICE │ │ AUTH-SERVICE │ │ WEB-FRONTEND │
│ GCS_CANVAS_ │ │ CLIENT_ID/ │ │ GATEWAY_URL │
│ BUCKET │ │ SECRET │ │ AUTH_SERVICE_ │
│ USER_MANAGER_ │ │ REDIRECT_URI │ │ URL │
│ URL │ │ WEB_FRONTEND_ │ └──────────────────┘
└──────────────────┘ │ URL │
└──────────────────┘
POST /discord/interactions- Discord bot interactionsPOST /web/interactions- Web client interactionsGET /health- Health checkGET /auth/*- Auth service endpointsGET /*- Web frontend pages
POST /discord/interactions- Receives Discord interactions (with signature verification)POST /web/interactions- Receives web client interactions (with session verification)GET /health- Health check
POST /canvas/draw- Draw a pixelGET /canvas/state- Get canvas state (JSON)POST /canvas/snapshot- Create a snapshotGET /canvas/stats- Canvas statisticsGET /canvas/pixel/{x}/{y}- Pixel informationGET /health- Health check
GET /api/users/{user_id}- Get a userPOST /api/users- Create/update a userPOST /api/users/{user_id}/increment- Increment usagePOST /api/rate-limit/check- Check rate limitGET /api/rate-limit/{user_id}- Rate limit informationGET /api/stats/leaderboard- LeaderboardGET /health- Health check
GET /auth/login- Start OAuth2 flowGET /auth/callback- OAuth2 callbackPOST /auth/verify- Verify a sessionGET /auth/user- Get current userPOST /auth/logout- LogoutGET /health- Health check
POST /- Receives Pub/Sub messages (via Eventarc trigger)GET /health- Health check (if implemented)
POST /register- Register all commandsPOST /register/{command_name}- Register specific commandGET /commands- List all defined commandsGET /health- Health check
-
Create base GCP resources
make setup-all
-
Create secrets
# See configs/gcp-secrets.json for complete list -
Create Pub/Sub topics
make setup-pubsub
-
Deploy data services
make deploy-user-manager make deploy-canvas-service
-
Deploy Proxy
make deploy-proxy
-
Deploy frontend services
make deploy-web-frontend make deploy-auth make deploy-registrar
-
Deploy Processors
make deploy-processors
-
Configure API Gateway
make update-gateway
-
Configure Discord
- Set API Gateway URL in Discord settings
https://guidon-*.ew.gateway.dev/discord/interactions
- Complete Isolation: Each service can crash without affecting others
- Scalability: Each service scales independently
- Maintenance: Update one service without affecting others
- Security: API Gateway as single entry point, private services with IAM
- Reliability: Pub/Sub guarantees message delivery
- Monitoring: Each service can be monitored separately
- Secret Centralization: All Discord secrets in proxy
- Separation of Concerns: Each service has a clear role
- Specialized Microservices: Each command has its own service
- Dedicated Data Layer: Canvas-service as single source of truth
- Direct Webhooks: Processors send responses directly, reducing latency
- Identity Token Authentication: Secure service-to-service communication