Chat with your OpenClaw AI assistant from anywhere, protected by authentication.
- 🔐 Authentication: OIDC (Authentik, Okta, Google) + local username/password fallback
- 💬 Real-time chat: WebSocket connection to OpenClaw gateway
- 📱 Mobile-friendly: PWA-style responsive UI with native app feel
- 🔔 Background alerts: Optional browser notifications + sounds for assistant replies when tab is hidden
- 🐳 Containerized: Docker + Kubernetes deployment ready
- 🔒 Security hardened: Non-root user, rate limiting, XSS protection
- 🤖 Automated: CI/CD with linting, testing, and multi-platform builds
- 📲 OTA Updates: Automatic over-the-air updates for native mobile apps (no external service required)
git clone https://github.com/misospace/miso-chat.git
cd miso-chat
cp .env.example .env
docker-compose up -ddocker run -d --name miso-chat \
-p 3000:3000 \
-v miso-chat-data:/app/data \
-e GATEWAY_URL=ws://your-gateway:18789 \
-e SESSION_SECRET=your-secret \
ghcr.io/misospace/miso-chat:latest
>**Note:** The image runs as a non-root `node` user and includes a healthcheck on `/api/health`.
> Mount `/app/data` to persist the SQLite database across container restarts.| Variable | Required | Default | Description |
|---|---|---|---|
GATEWAY_URL | Yes | - | WebSocket URL to OpenClaw gateway |
PORT | No | 3000 | Server port |
SESSION_SECRET | Yes | - | Secret for sessions |
SESSION_COOKIE_SAMESITE | No | strict (or lax when OIDC enabled) | Session cookie SameSite policy (`strict |
SESSION_COOKIE_SECURE | No | true in production | Override session cookie Secure flag |
CSRF_TRUSTED_ORIGINS | No | - | Comma-separated extra origins allowed for state-changing requests |
OIDC_ENABLED | No | false | Enable OIDC auth |
OIDC_ISSUER | If OIDC | - | OIDC issuer URL used for provider discovery |
OIDC_CLIENT_ID | If OIDC | - | OIDC client identifier |
OIDC_CLIENT_SECRET | If OIDC | - | OIDC client secret |
OIDC_CALLBACK_URL | Yes for production OIDC | - | Absolute HTTP(S) callback URL registered with the provider |
LOCAL_USERS | If local | admin:password123 | Users (user:pass) |
REDIS_URL | Yes in production | - | Redis/Dragonfly session store (required for production) |
ALLOW_MEMORY_STORE | No | false | Override production Redis requirement for development/testing |
CAPACITOR_COOKIES_ENABLED | No | true | Enable Capacitor cookie bridge for native app builds |
PUSH_NOTIFICATIONS_ENABLED | No | false | Reserved for future browser push support (production web-push NOT implemented) |
PUSH_VAPID_PUBLIC_KEY | If push enabled | - | Public VAPID key (reserved for future implementation) |
PUSH_VAPID_PRIVATE_KEY | If push enabled | - | Private VAPID key (reserved for future implementation) |
PUSH_VAPID_SUBJECT | If push enabled | - | Contact URI for VAPID claims (reserved for future implementation) |
TRUSTED_PROXY_IPS | No | empty | Comma-separated allowlist of reverse-proxy addresses whose forwarded headers (cf-connecting-ip, x-forwarded-for) may be used to derive rate-limit keys. Accepts plain IPv4/IPv6, CIDR blocks, and the cloudflare keyword. Defaults to empty — with no entry set, all rate-limit keys fall back to the direct TCP socket address and forwarded headers are ignored. See Trusted Proxies / Rate Limiting below. |
The miso-chat gateway WebSocket client requests OAuth scopes from the OpenClaw gateway on connect.
- Default scopes (least-privilege):
operator.read,operator.write— sufficient for normal chat, session list, history, send, and abort operations. - Admin/pairing scopes:
operator.admin,operator.pairing— only requested whenGATEWAY_ADMIN_SCOPES=trueis set in the environment.
This reduces blast radius: if a web/session bug exposes gateway capabilities, the default configuration cannot perform admin or pairing actions.
Note: Earlier versions of miso-chat included chat.send, sessions.send, sessions.list, and sessions.history in REQUESTED_GATEWAY_SCOPES. These are gateway method names, not valid OAuth scopes, and were removed as non-scope entries. The OpenClaw gateway rejects invalid scope names; normal chat/session operations only require operator.read + operator.write.
Deployments that need admin or pairing features (e.g., device pairing flows, admin tooling) should set:
-e GATEWAY_ADMIN_SCOPES=true \No code changes are required — the scope list is built at startup from the environment variable.
- Adds baseline HTTP hardening headers (
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy). - Enforces origin checks on
POST/PUT/PATCH/DELETErequests to reduce CSRF risk (configure extra trusted origins withCSRF_TRUSTED_ORIGINS). - Rate limiters (
/api/*,/api/events/stream,/auth/*) key only oncf-connecting-ip/x-forwarded-forwhen the direct TCP peer matchesTRUSTED_PROXY_IPS; otherwise the key is the socket address. See Trusted Proxies / Rate Limiting.
In earlier releases the rate limiters keyed on cf-connecting-ip first and x-forwarded-for second with no peer validation. With the app reachable directly on port 3000 (the default docker-compose deployment), a client could rotate those headers per request and mint an unlimited supply of rate-limit buckets, bypassing the auth limiter (20/15 min), the API limiter (100/15 min) and the SSE limiter (10/min).
This release introduces TRUSTED_PROXY_IPS:
- Empty (default): every request is keyed on its TCP socket address. Forwarded headers are ignored. This is the safe default for direct-publish deployments.
- Configured: forwarded headers are honored only when the direct TCP peer matches an entry.
cf-connecting-ipis honored only when the peer is a known Cloudflare edge range;x-forwarded-for(leftmost) is honored for any other trusted proxy.
Entries may be plain IPv4/IPv6 addresses, CIDR blocks, or the special keyword cloudflare (expands to the published Cloudflare edge ranges). Entries are normalized (lowercased, IPv4-mapped IPv6 ::ffff: prefix stripped) before comparison, so ::ffff:1.2.3.4 matches a peer reporting as 1.2.3.4.
# Behind Cloudflare only
TRUSTED_PROXY_IPS=cloudflare
# Behind an internal reverse proxy on 10.0.0.0/8 plus Cloudflare
TRUSTED_PROXY_IPS=10.0.0.0/8,cloudflare
# A single known peer
TRUSTED_PROXY_IPS=192.0.2.10
⚠️ Operators deploying behind Envoy, Cloudflare, nginx, or any reverse proxy must setTRUSTED_PROXY_IPS. With the default-empty allowlist, every request will appear to originate from the proxy's IP and all clients will share a single rate-limit bucket — a silent behavior change from the previoustrust proxy 1default.
securityContext:
runAsNonRoot: truerunAsUser: 1000readOnlyRootFilesystem: trueallowPrivilegeEscalation: falsecapabilities:
drop:
- ALLnpm install
npm run dev # Development
npm run test# Run tests
npm run lint # LintFor native APK builds, session persistence depends on cookie handling between the WebView and backend:
capacitor.config.jsnow enablesCapacitorCookiesby default (CAPACITOR_COOKIES_ENABLED=true).- If the app runs from an app origin (
capacitor://or custom scheme) and talks to a remote HTTPS API, set:SESSION_COOKIE_SAMESITE=noneSESSION_COOKIE_SECURE=true
401responses already trigger a login redirect in the client as a fallback when a session has expired.
Foreground browser notifications (tab-visible only; production web-push is NOT implemented yet).
- Open the top-right menu (
☰) and toggle Alerts on. - The app will ask for browser notification permission once.
- If notifications are blocked, allow them in site settings and re-enable Alerts.
Note: This uses the native Browser Notification API while the page is loaded. Production background web-push (via VAPID/SW) is reserved for future implementation.
Verify deploy health, login flow, and send-message API path:
SMOKE_BASE_URL="https://miso-chat.example.com" \
SMOKE_USERNAME="admin" \
SMOKE_PASSWORD="your-password" \
npm run smoke:deployOptional overrides:
SMOKE_SESSION_KEY(default:default)SMOKE_MESSAGE(default: timestamped smoke ping)SMOKE_HEALTH_URL(if health endpoint is not/api/health)SMOKE_TIMEOUT_SECONDS(default:20)
The GatewayWsManager includes automatic reconnection with exponential backoff:
- ✅ Tested: Connection loss recovery (server restart, network interruption)
- ✅ Tested: Exponential backoff delays (1s, 2s, 4s, 8s...)
- ✅ Tested: Max reconnection attempts limit (default: 5)
- ✅ Tested: Origin preservation across reconnections
- ✅ Tested: Event emission for
reconnecting,reconnect-error,reconnect-failed
Manual testing performed by:
- Starting miso-chat with active gateway connection
- Stopping the OpenClaw gateway service
- Observing reconnection attempts in logs
- Restarting gateway - connection automatically restored
See: lib/gateway-ws.js for implementation details (issue #111, parent #110)
Issue #115 - WebSocket connection persistence and behavior testing.
The Gateway connection maintains persistent state across disruptions:
| Scenario | Behavior | Status |
|---|---|---|
| Gateway restart | Auto-reconnect with exponential backoff | ✅ Tested |
| Network interruption (brief) | Reconnection after 1s → 2s → 4s... | ✅ Tested |
| Network interruption (extended) | Max 5 attempts, then reconnect-failed | ✅ Tested |
| Browser sleep/wake | Connection resumes if within retry window | ✅ Tested |
| Server-side close (1000/1001) | Clean close, no reconnect | ✅ Tested |
| Server-side error (1011) | Triggers reconnect sequence | ✅ Tested |
Persistence Configuration:
constmanager=newGatewayWsManager({maxReconnectAttempts: 5,// Max retries before giving upreconnectDelay: 1000,// Initial delay (ms)reconnectBackoff: 2,// Exponential multiplier// Max delay = 1000 * 2^4 = 16000ms (16s) on final attempt});Events for Monitoring:
manager.on('reconnecting',(attempt,delay)=>{console.log(`Reconnecting in ${delay}ms (attempt ${attempt})`);});manager.on('reconnect-failed',(err)=>{console.log('Giving up - manual intervention needed');});Pending Request Handling:
- In-flight requests during disconnect → timeout after 30s (configurable)
- Requests queued while disconnected → immediate error
- Successful reconnect does not retry failed requests (client responsibility)
GET /api/sessions- List all sessionsGET /api/sessions/:sessionKey/history- Get session message historyPOST /api/sessions/:sessionKey/send- Send a message
GET /api/reactions/:sessionKey- Get all reactions for a sessionGET /api/messages/:messageId/reactions- Get reactions for a message (optionally scoped with?sessionKey=...)POST /api/messages/:messageId/reactions- Toggle a reaction (add/remove)- Investigation notes for gateway reaction notifications:
docs/reaction-events-investigation.md
- Update version in package.json
- Ensure all tests pass (
npm test) - Run linting (
node --check server.js) - Check for security vulnerabilities (
npm audit) - Verify README.md is current (changelog, config table, OTA docs)
- Test WebSocket reconnection manually
- Restore shared session access
- Restore frontend startup
- Fix render-utils.js module not defined and Rocket Loader script conflict
- Release pipeline: support publish recovery, recognize app PR authors, detect missing tags, avoid tag visibility race
- Fix service worker serving stale HTML with old CSP headers
- Fix invalid action SHAs in android-release workflow
- Route manual-release through PR + auto-merge
- Remove CSP nonce that blocks inline scripts
- Commit missing JS files (present in Docker image but not git)
- Automate protected releases
- Harden mobile OTA manifest trust with validation
- Add session/operation authorization boundary and route-level CSRF tokens
- Consolidate update manager, remove stale APK logic
- Add SESSION_COOKIE_DOMAIN env var for subdomain isolation
- Repair release/docs drift (changelog + OTA docs)
- Make lint actually run ESLint on server.js, lib/, and tests/
- Add authorization/integration test matrix
- Harden container runtime defaults
- Added release runbook to AGENTS.md
- Updated
@capgo/capacitor-updater8.47.5 → 8.47.6 - Updated
actions/checkoutCI action v6.0.2 → v6.0.3
- Fixed README (README drift corrections)
- fix(ws): infer
GATEWAY_WS_ORIGINfromCORS_ORIGINif not set
- feat(chat): render YouTube embeds inline
- feat: add issue templates
- feat: add GPL-3.0 license
- fix(readme): update docker badge to ghcr
- fix(mobile): reload page after mobile auth callback to prevent 401 loop
- Migrated from
joryirvingtomisospaceorg - Feat/rename claude
- Fix xmldom high severity vulnerability
- Cap WebSocket reconnect backoff delay (#446)
- Add AGENTS.md for agent guidance
- Security audit fixes for issue #449
- fix(mobile): restore native onboarding and OTA affordance
- fix(chat): strip tool output from rendered history
- fix(chat): partial fix for #407 live send sanitization
- fix(mobile): stop auth loop and hide tool output
- fix(mobile): restore onboarding and auth callback flow
- fix(ui): restore grouped session picker and agent naming
- fix(chat): bypass broken stream send queue path
- Fix #354: Integrate OTA update manager and add documentation
- fix(chat): add retry controls for failed sends
- fix(chat): prevent duplicate pending messages on reconnect (fixes #365)
- Fix #363: Remove group chat UI and related flows
- fix(rate-limit): use express-rate-limit ipKeyGenerator for IPv6-safe key generation
- fix(chat): remove unsupported attachment composer affordance
- fix(chat): clarify connecting status text as OpenClaw gateway
- fix(chat): clarify OpenClaw Gateway URL in settings copy
- feat(release): add one-click manual release workflow
- fix(chat): clarify reaction buttons are local-only via aria-label
- feat(release): use bot auth for one-click release
- fix(server): restore release build startup banner
- fix(ci): repair release auth-smoke startup
- fix(ci): restore session api routes for auth smoke
- fix(chat): restore UI/gateway contract regressions
- fix(ws): use gateway-compatible client id
- fix(ws): send gateway device auth during connect
- fix(ui): restore header control handlers
- fix(chat): restore assistant header and message rendering
- fix(chat): restore gateway send and history contract
- fix(chat): send correct fallback message payload
- fix: handle mobile auth callback on cold app launch
- fix(debug): show mobile auth traces in browser console on web
- fix(ota): publish web bundles to Capgo + default auto updates
- feat(ota): self-hosted Capacitor OTA via GitHub release assets
- Added auto-bump workflow for package.json
- Added debug logging to mobile auth endpoint
- ci: generate and manage release notes via separate workflow
- Fix version display and add group chat feature
- fix: resolve 0.4.1 'Connecting...' frontend regression
- fix: checkout release tag before reading version in android-release workflow
- fix: enhance message queue persistence with edge case handling
- feat: Add streaming response support via /send-stream endpoint
- docs: Add comprehensive wishlist improvement suggestions
- feat: add version number to footer
- Fix: Include all session kinds in sessions list
- feat: add multi-agent group chat endpoint
- WebSocket reconnection with session state recovery
- fix: prevent auto-login after logout by forcing re-authentication
- feat: Add loading spinner for tool calls in progress
- fix: Improve notification sound playback and tab title updates (#306)
- #125: Add reaction counts like Discord - reactions now show count badges
- #126: Add dark/light theme toggle with localStorage persistence
- Improved WebSocket reconnection on errors
- Fixed emoji picker background and positioning
- Typing indicator now responds to gateway events only
- Initial release
- OIDC authentication support
- Real-time WebSocket chat
- Mobile-friendly PWA UI
MIT License - see LICENSE.
When running behind Envoy/Ingress over HTTPS, this app trusts one proxy hop for secure session cookies.
WebSocket upgrades require an authenticated session (unauthenticated upgrades are rejected with 401).
With OIDC enabled,
/loginredirects directly to/auth/oidc.OIDC discovery runs on the first login request and reports configuration failures through the login error flow.
If you enable the dragonfly component in home-ops, set:
env:
REDIS_URL: "redis://{{ .Release.Name }}-dragonfly:6379"In production, REDIS_URL is required — without it, miso-chat will refuse to start (fail-fast). This prevents silent data loss from in-memory session stores across restarts or multi-instance deployments.
For local development and testing, the in-memory store is used by default. If you need MemoryStore in production for a specific reason, set ALLOW_MEMORY_STORE=true to override the check.
Miso Chat supports automatic over-the-air (OTA) updates for native mobile apps using the self-hosted Capgo Capacitor Updater. No external service or API key required — updates are served from GitHub releases.
- When a new release is published on GitHub, the update manager checks for updates
- The server serves
update-manifest.jsonfrom the latest release viaGET /api/mobile/update-manifest - If an update is available, you'll see a notification in the app
- Tap "Update Now" to download and install the update
- The app restarts with the new version
- Native mobile app build (Android APK or iOS app)
- Capacitor platform with
@capgo/capacitor-updaterplugin installed - GitHub release with
update-manifest.jsonasset
Since v0.4.x, miso-chat serves the update manifest from its own endpoint rather than having clients hit the GitHub API directly:
- Endpoint:
GET /api/mobile/update-manifest - Cache: Server caches the manifest for 5 minutes (configurable)
- Fallback: Client falls back to direct GitHub API lookup if the server is unavailable
Environment Variables:
| Variable | Default | Description |
|---|---|---|
MOBILE_UPDATE_REPO_OWNER | misospace | GitHub org for update releases |
MOBILE_UPDATE_REPO_NAME | miso-chat | GitHub repo for update releases |
MOBILE_UPDATE_CACHE_TTL_MS | 300000 | Cache TTL in milliseconds (5 min) |
To switch to a different release source, set MOBILE_UPDATE_REPO_OWNER and MOBILE_UPDATE_REPO_NAME.
To manually check for updates, call:
awaitMobileUpdateManager.checkForUpdate();The update notification appears when:
- A new version is available
- The app is running on a native mobile platform
showNotificationis enabled in the config
The update manager can be configured with:
awaitMobileUpdateManager.init({autoCheck: true,// Automatically check for updates on startupshowNotification: true,// Show update notification when availablecheckInterval: 3600000,// Check every hour (default)debug: false// Enable debug logging});To publish an update, use the Manual Release GitHub Actions workflow and enter a version like 0.4.15. It normalizes v0.4.15 to 0.4.15, opens a version-bump PR, and enables auto-merge. After the required checks pass, Publish Release tags the merged commit and creates the GitHub release with generated notes.
- Run the
Manual Releaseworkflow with the target version - Follow the linked version-bump PR; it merges automatically after required checks pass
- The publish workflow creates the plain-semver git tag (for example
0.4.15) - Attach the APK/IPA and
update-manifest.jsonto the created GitHub release if needed - The update manager will automatically detect the new version
Updates not appearing?
- Check that the GitHub release includes
update-manifest.json - Verify the version number in
package.jsonis higher than the current version - Check browser console for update manager errors
Update failed?
- Ensure the APK/IPA is properly signed
- Check network connectivity
- Verify the
update-manifest.jsoncontains validbundleUrl