# Alfred Mobile Architecture **Complete system architecture showing authentication, networking, and communication flow** > **Note:** This document uses example IPs (10.0.1.x) and domains (example.com) for privacy. Replace with your actual values when deploying. ## System Overview ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ MOBILE DEVICE │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ Alfred Mobile App │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ │ │ AuthUI │ │ GatewayClient│ │ SharedPreferences │ │ │ │ │ │ (WebView) │ │ (WebSocket) │ │ - OAuth tokens │ │ │ │ │ │ │ │ │ │ - Token expiry │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────────────────┘ │ │ │ │ │ │ │ │ │ └─────────┼─────────────────┼─────────────────────────────────────┘ │ │ │ │ │ └────────────┼─────────────────┼───────────────────────────────────────────┘ │ │ │ HTTPS │ WSS (WebSocket Secure) │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ INTERNET / NETWORK │ └─────────────────────────────────────────────────────────────────────────┘ │ │ │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ HAProxy (10.0.1.20) │ │ ┌────────────────────────┐ │ │ │ SSL Termination │ │ │ │ - alfred-app. │ │ │ │ example.com:443 │ │ │ └────────┬───────────────┘ │ │ │ │ │ │ HTTP/WS (no SSL) │ │ │ │ └─────────────────────────────┼────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Windows Desktop (10.0.1.100) │ │ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ OAuth Proxy (Node.js) │ │ │ │ Port: 18790 (LAN-accessible) │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ │ │ HTTP Endpoint: /api/notify │ │ │ │ │ │ - Accepts notification POST requests │ │ │ │ │ │ - Broadcasts to connected WebSocket clients │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ │ │ WebSocket Handler │ │ │ │ │ │ 1. Validates OAuth token with Authentik │ │ │ │ │ │ 2. Connects to OpenClaw gateway (localhost) │ │ │ │ │ │ 3. Injects OpenClaw token into messages │ │ │ │ │ │ 4. Proxies bidirectional traffic │ │ │ │ │ │ 5. Tracks clients for notification broadcasting │ │ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ Connected Clients Set: │ │ │ │ • Tracks active WebSocket connections │ │ │ │ • Used for broadcast notifications │ │ │ └────────┬─────────────────────────────────────┬──────────────────┘ │ │ │ │ │ │ │ Validates │ Connects │ │ │ OAuth token │ (localhost only) │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ │ Authentik │ │ OpenClaw Gateway │ │ │ │ (10.0.1.75) │ │ Port: 18789 │ │ │ │ OAuth2 Provider │ │ Bind: loopback (127.0.0.1)│ │ │ │ │ │ Token: 9b87d1... │ │ │ │ - Client ID │ │ │ │ │ │ - Validates tokens │ │ ┌──────────────────────┐ │ │ │ │ - Returns userinfo │ │ │ Session: main │ │ │ │ └─────────────────────┘ │ │ - Chat interface │ │ │ │ │ │ - Agent tools │ │ │ │ │ └──────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────┐ │ │ │ │ │ mobile-notify skill │ │ │ │ │ │ - Sends to proxy │ │ │ │ │ └──────────────────────┘ │ │ │ └────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────────────┘ ``` ## Authentication Flow ``` ┌──────────────┐ │ Mobile App │ │ (Starts) │ └──────┬───────┘ │ │ 1. No token? Launch OAuth flow │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ AuthUI (WebView) │ │ https://auth.example.com/application/o/authorize/ │ │ ?client_id=YOUR_OAUTH_CLIENT_ID │ │ &redirect_uri=alfredmobile://oauth/callback │ │ &response_type=code │ │ &scope=openid profile email │ └──────┬───────────────────────────────────────────────────────┘ │ │ 2. User logs in via Authentik │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Authentik OAuth Provider (10.0.1.75) │ │ - Validates credentials │ │ - Issues authorization code │ │ - Redirects: alfredmobile://oauth/callback?code=AUTH_CODE_EXAMPLE │ └──────┬───────────────────────────────────────────────────────┘ │ │ 3. App captures redirect │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ AuthManager.handleCallback() │ │ - Extracts authorization code │ │ - Exchanges code for tokens │ └──────┬───────────────────────────────────────────────────────┘ │ │ 4. POST to token endpoint │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ POST https://auth.example.com/application/o/token/ │ │ { │ │ grant_type: "authorization_code", │ │ code: "AUTH_CODE_EXAMPLE", │ │ client_id: "QeSNa...", │ │ redirect_uri: "alfredmobile://oauth/callback" │ │ } │ └──────┬───────────────────────────────────────────────────────┘ │ │ 5. Receives tokens │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Token Response: │ │ { │ │ access_token: "YOUR_JWT_TOKEN...", │ │ token_type: "Bearer", │ │ expires_in: 3600, │ │ refresh_token: "YOUR_REFRESH_TOKEN...", │ │ id_token: "YOUR_JWT_TOKEN..." │ │ } │ └──────┬───────────────────────────────────────────────────────┘ │ │ 6. Store in SharedPreferences │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ SharedPreferences (Android) │ │ - Key: "access_token" → "YOUR_JWT_TOKEN..." │ │ - Key: "token_expiry" → 1707089234567 (timestamp) │ │ - Key: "refresh_token" → "YOUR_REFRESH_TOKEN..." │ │ │ │ Storage location: /data/data/com.openclaw.alfred/ │ │ shared_prefs/auth_prefs.xml │ └──────┬───────────────────────────────────────────────────────┘ │ │ 7. Token valid? Connect to gateway │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ GatewayClient.connect() │ │ - WebSocket: wss://alfred-app.example.com │ │ - Header: Authorization: Bearer YOUR_JWT_TOKEN... │ └───────────────────────────────────────────────────────────────┘ ``` ## WebSocket Communication Flow ``` ┌──────────────┐ │ Mobile App │ │ (Connected) │ └──────┬───────┘ │ │ Send message │ ▼ ┌────────────────────────────────────────────────────────────┐ │ WebSocket Message (WSS) │ │ wss://alfred-app.example.com │ │ │ │ { │ │ type: "req", │ │ id: "chat-1", │ │ method: "chat.send", │ │ params: { │ │ sessionKey: "main", │ │ message: "What's the weather?" │ │ } │ │ } │ └──────┬─────────────────────────────────────────────────────┘ │ │ TLS encrypted │ ▼ ┌────────────────────────────────────────────────────────────┐ │ HAProxy (10.0.1.20) │ │ - Terminates TLS │ │ - Forwards to: http://10.0.1.100:18790 │ └──────┬─────────────────────────────────────────────────────┘ │ │ Plain HTTP/WS │ ▼ ┌────────────────────────────────────────────────────────────┐ │ OAuth Proxy (10.0.1.100:18790) │ │ │ │ 1. Extract OAuth token from Authorization header │ │ Token: YOUR_JWT_TOKEN... │ │ │ │ 2. Validate with Authentik │ │ GET https://auth.example.com/application/o/userinfo/ │ │ Authorization: Bearer YOUR_JWT_TOKEN... │ │ │ │ 3. Authentik responds with user info │ │ { sub: "...", email: "user@...", ... } │ │ │ │ 4. Valid? Connect to OpenClaw gateway │ │ ws://127.0.0.1:18789 │ │ │ │ 5. Proxy receives message from mobile app │ │ │ │ 6. Inject OpenClaw token │ │ If message is connect request: │ │ params.auth.token = "9b87d15f..." │ │ │ │ 7. Forward to OpenClaw │ └──────┬─────────────────────────────────────────────────────┘ │ │ Localhost only │ ▼ ┌────────────────────────────────────────────────────────────┐ │ OpenClaw Gateway (127.0.0.1:18789) │ │ │ │ 1. Receives message with injected token │ │ │ │ 2. Validates OpenClaw token │ │ Token: 9b87d15f... ✓ │ │ │ │ 3. Routes to agent session "main" │ │ │ │ 4. Agent processes message │ │ Message: "What's the weather?" │ │ │ │ 5. Agent generates response │ │ Response: "Currently 54°F in Your City..." │ │ │ │ 6. Sends chat event back │ │ { │ │ type: "event", │ │ event: "chat", │ │ payload: { │ │ message: { role: "assistant", content: [...] } │ │ } │ │ } │ └──────┬─────────────────────────────────────────────────────┘ │ │ Response flows back through proxy │ ▼ ┌────────────────────────────────────────────────────────────┐ │ OAuth Proxy │ │ - Receives response from OpenClaw │ │ - Forwards to mobile app (pass-through) │ └──────┬─────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────┐ │ HAProxy │ │ - Wraps in TLS │ │ - Sends to mobile device │ └──────┬─────────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────┐ │ Mobile App │ │ - GatewayClient.onMessage() │ │ - Updates UI: "Currently 54°F in Your City..." │ └────────────────────────────────────────────────────────────┘ ``` ## Notification Flow (mobile-notify tool) ``` ┌────────────────────────────────────────────────────────────┐ │ Alfred Agent (OpenClaw) │ │ │ │ User says: "Remind me to check the laundry in 15 minutes" │ │ │ │ Agent executes: │ │ exec(['mobile-notify', 'timer', '15m', │ │ 'Check the laundry']) │ └──────┬─────────────────────────────────────────────────────┘ │ │ Shell execution │ ▼ ┌────────────────────────────────────────────────────────────┐ │ mobile-notify CLI tool │ │ ~/.openclaw/workspace/skills/mobile-notify/ │ │ │ │ 1. Parse command: timer "15m" "Check the laundry" │ │ │ │ 2. For timers/reminders: Schedule via cron │ │ POST http://localhost:18789/api/cron/add │ │ { │ │ schedule: { kind: "at", atMs: 1707089234567 }, │ │ payload: { │ │ kind: "agentTurn", │ │ message: "Execute: mobile-notify alert ..." │ │ } │ │ } │ │ │ │ 3. For instant alerts: Send immediately │ │ POST http://localhost:18790/api/notify │ └──────┬─────────────────────────────────────────────────────┘ │ │ HTTP POST (for instant alerts) │ ▼ ┌────────────────────────────────────────────────────────────┐ │ OAuth Proxy: /api/notify endpoint │ │ │ │ { │ │ notificationType: "alert", │ │ title: "Alfred", │ │ message: "Check the laundry", │ │ priority: "default", │ │ sound: true, │ │ vibrate: true │ │ } │ │ │ │ 1. Create notification event │ │ 2. Broadcast to all connected WebSocket clients │ │ connectedClients.forEach(client => { │ │ client.send(JSON.stringify({ │ │ type: "event", │ │ event: "mobile.notification", │ │ payload: {...} │ │ })) │ │ }) │ └──────┬─────────────────────────────────────────────────────┘ │ │ WebSocket broadcast │ ▼ ┌────────────────────────────────────────────────────────────┐ │ All Connected Mobile Clients │ │ │ │ GatewayClient.handleEvent() │ │ event: "mobile.notification" │ │ → GatewayListener.onNotification() │ │ │ │ MainScreen.onNotification() │ │ 1. Add icon based on type (⏰ ⚠️ 🔔) │ │ 2. Show system notification (background OR timer/remind) │ │ NotificationHelper.showNotification() │ │ 3. Add to chat if foreground │ │ 4. Optional TTS if enabled │ └────────────────────────────────────────────────────────────┘ ``` ## Token Storage Locations ### Mobile App (Android) ``` File: /data/data/com.openclaw.alfred/shared_prefs/auth_prefs.xml eyJhbGciOiJSUzI1NiIs... 1707089234567 YOUR_REFRESH_TOKEN... Access: AuthManager.kt - getAccessToken() - isTokenExpired() - refreshToken() ``` ### OAuth Proxy (Memory) ``` Location: In-memory Set (JavaScript) const connectedClients = new Set(); Each WebSocket connection: - OAuth token validated once on connect - Connection stored in Set for broadcasts - Removed from Set on disconnect ``` ### OpenClaw Gateway ``` Location: ~/.openclaw/config.json { "gateway": { "token": "YOUR_OPENCLAW_TOKEN", "port": 18789, "bind": "loopback" } } Used by: OAuth Proxy only (localhost connection) ``` ### Authentik (Database) ``` Location: PostgreSQL database (10.0.1.75) Tables: - oauth2_provider_accesstoken - oauth2_provider_refreshtoken - oauth2_provider_authorizationcode Validates tokens via userinfo endpoint ``` ## Network Topology ``` Internet │ └─── Router/Firewall (10.0.1.1) │ ├─── HAProxy VM (10.0.1.20:443) │ - SSL termination │ - Reverse proxy │ ├─── Authentik (10.0.1.75:443) │ - OAuth2 provider │ - User authentication │ └─── Windows Desktop (10.0.1.100) - WSL Ubuntu 22.04 │ ├─── OAuth Proxy (port 18790) │ - LAN-accessible │ - Validates & proxies │ └─── OpenClaw Gateway (port 18789) - Localhost-only - Main agent session ``` ## Security Model ### Defense in Depth 1. **Mobile App** - OAuth tokens stored in encrypted SharedPreferences - Token expiry validation (30s buffer) - Automatic refresh on expiry 2. **Transport Security** - TLS 1.3 encryption (mobile → HAProxy) - Valid SSL certificate - HTTPS/WSS only for external connections 3. **HAProxy** - SSL termination - Rate limiting (if configured) - IP filtering (if configured) 4. **OAuth Proxy** - Validates OAuth token on every connection - No token caching (validates with Authentik) - Localhost-only connection to OpenClaw - Injects OpenClaw token (never exposed to client) 5. **OpenClaw Gateway** - Bind: loopback (127.0.0.1 only) - Token authentication required - Not directly accessible from network 6. **Authentik** - OAuth2 standard compliance - Secure token generation - User session management ### Token Security **OAuth Token (Mobile ↔ Proxy)** - Short-lived (1 hour) - Refresh token rotation - Validated on every connection - Stored securely on device **OpenClaw Token (Proxy ↔ Gateway)** - Static token (configured) - Never leaves localhost - Never sent to mobile clients - Injected by proxy ## File Locations ### Mobile App ``` ~/.openclaw/workspace/alfred-mobile/ ├── app/src/main/java/com/openclaw/alfred/ │ ├── auth/AuthManager.kt (OAuth handling) │ ├── gateway/GatewayClient.kt (WebSocket client) │ ├── ui/screens/MainScreen.kt (UI + notification handler) │ ├── notifications/NotificationHelper.kt │ └── storage/ConversationStorage.kt ├── ARCHITECTURE.md (this file) └── secrets.properties (OAuth client ID) ``` ### OAuth Proxy ``` ~/.openclaw/workspace/alfred-proxy/ ├── server.js (Main proxy logic) ├── .env (Config + secrets) └── /tmp/alfred-proxy.log (Runtime logs) ``` ### OpenClaw ``` ~/.openclaw/ ├── config.json (Gateway token + config) └── workspace/skills/mobile-notify/ (Notification tool) ├── mobile-notify (CLI wrapper) └── scripts/notify.js (Implementation) ``` ### System Services ``` /etc/systemd/system/alfred-proxy.service (if using systemd) /tmp/alfred-proxy.log (proxy logs) ``` ## Ports Summary | Service | Port | Bind | Access | Protocol | |---------|------|------|--------|----------| | HAProxy | 443 | 0.0.0.0 | External | HTTPS/WSS | | Authentik | 443 | 10.0.1.75 | LAN | HTTPS | | OAuth Proxy | 18790 | 0.0.0.0 | LAN | HTTP/WS | | OpenClaw Gateway | 18789 | 127.0.0.1 | Localhost | HTTP/WS | ## URLs | Purpose | URL | |---------|-----| | Mobile app connection | `wss://alfred-app.example.com` | | OAuth authorize | `https://auth.example.com/application/o/authorize/` | | OAuth token | `https://auth.example.com/application/o/token/` | | OAuth userinfo | `https://auth.example.com/application/o/userinfo/` | | OAuth redirect | `alfredmobile://oauth/callback` | | Proxy health check | `http://10.0.1.100:18790/health` | | Proxy notify API | `http://10.0.1.100:18790/api/notify` | ## Key Design Decisions 1. **Two-token architecture** - OAuth token: Mobile app authentication - OpenClaw token: Backend service authentication - Separation of concerns + security 2. **Proxy pattern** - Mobile app never has direct OpenClaw access - Token injection at proxy layer - Enables centralized validation 3. **Localhost-only OpenClaw** - Reduces attack surface - Proxy is single point of entry - Gateway not exposed to network 4. **Notification broadcasting** - All clients receive notifications - Supports multiple devices - Real-time push via WebSocket 5. **SSL termination at HAProxy** - Centralized certificate management - Backend uses plain HTTP (trusted LAN) - Standard reverse proxy pattern ## Version 1.0.0 - Initial architecture (February 2026)