WebSocket relay server for Shellular — relays messages between Shellular CLI host agent and the Shellular app client.
- Node.js 18+
- pnpm
pnpm installCreate a local .env from .env.example. The server requires WS_TOKEN_SECRET with at least 32 characters for signing short-lived app WebSocket tickets.
OAuth provider settings live in .env. Apple Sign in with Apple uses APPLE_CLIENT_ID, APPLE_TEAM_ID, and APPLE_KEY_ID, and reads the private key from the ignored apple_key.p8 file in the server directory.
The server is two independently-deployed processes under one monorepo:
- central (
central/) — auth, DB (SQLite), token minting, the relay/presence registry, and HTTP routes. Runs no WebSocket relay. - relay (
relay/) — a DB-free regional relay that verifies signed tokens and just relays messages between the CLI and app. Deploy one per region. The relay registers itself with central and reports presence and stats.
Scripts are prefixed central: / relay: so it's always explicit which one you're running.
pnpm run dev# start (named "shellular-server-central")
pnpm run central:pm2:start
# restart
pnpm run central:pm2:restart# start (named "shellular-server-relay")
pnpm run relay:pm2:start
# restart
pnpm run relay:pm2:restartSee docs/oauth-flow.md for provider setup, token lifecycle, and the app/WebSocket authentication flow.
The app no longer sends access tokens or device metadata in the /app WebSocket URL. Instead:
- The app refreshes its access token.
- The app sends
POST /auth/ws-app-tokenwithAuthorization: Bearer <accessToken>and client metadata in the JSON body. - The server validates the user token,
ClientInfoSchema, host existence, and known-client identity. - The server returns a signed app WebSocket ticket that expires after 30 seconds.
- The app opens
/app?wsToken=<ticket>.
The /cli WebSocket and /host/register flow are unchanged.
Short, dismiss-once messages shown as a popup on the app home screen (e.g. maintenance heads-ups). Notices are served from a plain JSON file so they can be pushed to already-installed apps without an app update.
How it works
- Notices live in
content/notices.json. - The server reads that file into memory on boot and re-reads it about once an hour, so edits show up in the app without a redeploy or restart. A malformed edit keeps the previous cache rather than taking the endpoint down.
GET /noticesreturns the cached notices; the app fetches this and shows the first notice the user hasn't dismissed.- Each notice has a stable
id. The app remembers which ids a user dismissed, so a dismissed notice never reappears — but adding a new notice (new id) pops up for everyone. To push a fresh message, add a new notice.
Managing notices
Use the notices CLI rather than hand-editing the JSON — it owns id generation so ids are never typed by hand.
# List all notices
pnpm notices list
# Add a notice interactively — prompts for title and body (id is generated automatically)
pnpm notices add
# Add a notice non-interactively with flags
pnpm notices add --title "Heads up" --body "Some message"
# Edit an existing notice's title and/or body
pnpm notices edit <id> --title "New title" --body "New body"
# Remove a notice
pnpm notices rm <id>Changes take effect the next time the server re-reads the file (within ~1 hour), or immediately on restart.
AGPL-3.0-only
- CLI host agent: https://www.npmjs.com/package/shellular
- Issues / feedback: team@shellular.dev