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 \
-e GATEWAY_URL=ws://your-gateway:18789 \
-e SESSION_SECRET=your-secret \
ghcr.io/misospace/miso-chat:latest| 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 |
LOCAL_USERS |
If local | admin:password123 |
Users (user:pass) |
REDIS_URL |
No | - | Optional Redis/Dragonfly session store |
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) |
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).
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
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:
const manager = new GatewayWsManager({
maxReconnectAttempts: 5, // Max retries before giving up
reconnectDelay: 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 CHANGELOG.md with changes
- Update version in package.json
- Ensure all tests pass (
npm test) - Run linting (
npm run lint) - Check for security vulnerabilities (
npm audit) - Verify README.md is current
- Test WebSocket reconnection manually
- #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. -
Startup validation fails fast when OIDC is enabled but required env vars are missing.
If you enable the dragonfly component in home-ops, set:
env:
REDIS_URL: "redis://{{ .Release.Name }}-dragonfly:6379"This is optional. If REDIS_URL is not set, miso-chat falls back to in-memory sessions.
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.
- When a new release is published on GitHub, the update manager automatically checks for updates
- If an update is available, you'll see a notification in the app
- Tap "Update Now" to download and install the update
- The app will restart 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
To manually check for updates, call:
await MobileUpdateManager.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:
await MobileUpdateManager.init({
autoCheck: true, // Automatically check for updates on startup
showNotification: true, // Show update notification when available
checkInterval: 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.6. It normalizes v0.4.6 to 0.4.6, bumps package.json, commits the bump to main, creates the plain-semver tag, and creates the GitHub release with generated notes.
- Run the
Manual Releaseworkflow with the target version - The workflow updates
package.jsononmain - The workflow creates the plain-semver git tag (for example
0.4.6) - 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