โ ๏ธ This project is a work in progress and is subject to change at any time. Features, APIs, and behavior may be modified or removed without notice.Features added to Sprites will likely make some of the hacks described below redundant, and hopefully a lot of this, especially setup, configuration, and orchestration, will be simplified in the near future.
This is a personal project, not an official Fly.io product.
sprite-mobile gives you a progressive web app chat UI for accessing Claude Code running in YOLO mode on a Sprite, an ideal vibe-coding interface on your phone. It allows input by text, voice, and image, persists sessions across clients, and seamlessly networks with your other sprites through Tailscale.
- Sprite Setup
- Sprite Orchestration
- Prerequisites
- Claude Code Integration
- Features
- Access Model
- Quick Start
- Environment Variables
- Architecture
- Session Lifecycle
- Configuration
- Security
- Troubleshooting
- License
To set up a fresh Sprite with all dependencies, authentication, and services, download and run the setup script:
curl -fsSL https://raw.githubusercontent.com/clouvet/sprite-mobile/refs/heads/main/scripts/sprite-setup.sh -o sprite-setup.sh && chmod +x sprite-setup.sh && ./sprite-setup.shThe script will:
- Install Sprites CLI and authenticate
- Configure hostname, git user, URLs, and repo (auto-detects public URL from sprite metadata)
- Authenticate Claude CLI
- Authenticate GitHub CLI
- Install Fly.io CLI
- Install and configure Tailscale
- Set up Tailscale Serve (HTTPS for PWA support)
- Install and configure claude-hub (WebSocket hub for multi-client sync)
- Clone and run sprite-mobile
- Set up Sprite Network credentials (optional - enables automatic discovery of other sprites in your org)
- Start the Tailnet Gate (public entry point that embeds Tailscale URL via iframe)
- Create CLAUDE.md with sprite environment instructions
The script is idempotent and can be safely re-run.
The app is installed to ~/.sprite-mobile (hidden directory). On each service start, it attempts to auto-update via git pull so all sprites receive updates when they wake up.
Note: During authentication:
- Claude CLI may start a new Claude session after completing. Just type
exitor pressCtrl+Cto exit and continue.
Once you have one sprite-mobile sprite set up, it can automatically create and configure new sprites with a single command. This is useful for scaling your sprite fleet or letting Claude Code create new sprites on demand.
For fully automated sprite creation, you need:
- ~/.sprite-config file - Created automatically during initial setup
- Tailscale reusable auth key - Must be saved in your
~/.sprite-config - Authenticated CLI tools - Claude, GitHub, Fly.io, and Sprite CLI
One-Time Setup: Tailscale Reusable Auth Key
Create a reusable auth key and add it to your ~/.sprite-config:
- Go to https://login.tailscale.com/admin/settings/keys
- Click "Generate auth key"
- Check "Reusable"
- Copy the key and add it to
~/.sprite-config:TAILSCALE_AUTH_KEY=tskey-auth-xxxxx
From any existing sprite-mobile sprite:
~/.sprite-mobile/scripts/create-sprite.sh my-new-spriteThat's it! This single command will:
- Create a new sprite with the given name
- Make its URL public
- Transfer your
.sprite-configto the new sprite (excluding sprite-specific URLs) - Download and run the full setup script non-interactively
- Verify all services are running
Example output:
Creating and Configuring Sprite
Target sprite: my-new-sprite
Step 1: Creating sprite...
Created sprite: my-new-sprite
Step 2: Making URL public...
Public URL: https://my-new-sprite.sprites.app
Step 3: Transferring configuration...
Transferred ~/.sprite-config (excluded sprite-specific URLs)
Step 4: Downloading setup script...
Downloaded sprite-setup.sh
Step 5: Running setup script (this may take 3-5 minutes)...
[Setup runs automatically with your credentials]
Setup Complete!
The script transfers your ~/.sprite-config which includes:
- Git configuration (user.name, user.email)
- Claude CLI OAuth token
- GitHub CLI token
- Fly.io API token
- Sprite API token
- Tailscale reusable auth key
- Sprite Network credentials
The following are unique per sprite and NOT transferred:
SPRITE_PUBLIC_URL- Stripped during transfer, set correctly for the new spriteTAILSCALE_SERVE_URL- Stripped during transfer, generated during setup- Hostname - Set to the sprite name automatically
The create-sprite.sh script uses a defense-in-depth approach:
-
Filters sprite-specific values during config transfer:
# Strip SPRITE_PUBLIC_URL and TAILSCALE_SERVE_URL grep -v '^SPRITE_PUBLIC_URL=' ~/.sprite-config | \ grep -v '^TAILSCALE_SERVE_URL=' > filtered-config
-
Passes correct values to setup script:
sprite exec -- ./sprite-setup.sh --name 'my-new-sprite' --url 'https://my-new-sprite.sprites.app' all
This ensures the new sprite always gets the correct public URL and hostname, even if the source config contained different values.
If you prefer manual control or need to customize the process:
# 1. Create sprite
sprite create my-new-sprite
# 2. Make URL public and get the URL
sprite url update --auth public -s my-new-sprite
PUBLIC_URL=$(sprite api /v1/sprites/my-new-sprite | grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"\([^"]*\)".*/\1/')
# 3. Transfer config (excluding sprite-specific URLs)
grep -v '^SPRITE_PUBLIC_URL=' ~/.sprite-config | grep -v '^TAILSCALE_SERVE_URL=' | \
sprite -s my-new-sprite exec -- cat > ~/.sprite-config
# 4. Download and run setup
sprite -s my-new-sprite exec -- bash -c "
curl -fsSL https://gist.githubusercontent.com/clouvet/901dabc09e62648fa394af65ad004d04/raw/sprite-setup.sh -o ~/sprite-setup.sh
chmod +x ~/sprite-setup.sh
~/sprite-setup.sh --name my-new-sprite --url '$PUBLIC_URL' all
"With orchestration configured, you can simply tell Claude Code:
"Create a new sprite-mobile sprite called test-sprite"
Claude will use create-sprite.sh to handle the entire process automatically.
This app is designed to run on a Sprite from sprites.dev. Sprites come with:
- Bun runtime pre-installed
- Claude Code CLI pre-installed and authenticated
If running elsewhere, you'll need to install these manually and authenticate Claude Code with claude login.
sprite-mobile includes a comprehensive Claude skill that provides context about the architecture, service management, development workflows, and sprite orchestration. When working with Claude Code on a sprite-mobile sprite, Claude automatically has access to this skill.
The skill covers:
- Architecture (tailnet-gate + sprite-mobile integration)
- Service management (restart procedures, logs, status)
- Development workflows (service worker cache versioning)
- Creating and managing other sprite-mobile sprites
- Configuration management and replication
- API endpoints and WebSocket protocol
- Common troubleshooting tasks
Location: .claude/skills/sprite-mobile.md
This means you can ask Claude questions like:
- "How do I restart the sprite-mobile service?"
- "Create a new sprite-mobile sprite called test-sprite"
- "What's the service worker cache version and when should I bump it?"
- "How does the tailnet-gate work?"
Claude will have full context about sprite-mobile without needing to read through documentation or search for files.
- Multiple Chat Sessions: Create and manage multiple independent chat sessions, each with its own Claude Code process
- Persistent History: Messages are saved to disk and survive server restarts
- Session Resume: Reconnecting to a session resumes the existing Claude conversation
- Image Support: Upload and send images to Claude for analysis (auto-resized for API limits)
- Real-time Streaming: Responses stream in real-time via WebSocket
- Activity Indicators: See exactly what Claude is doing (reading files, running commands, searching)
- Multi-client Support: Multiple browser tabs can connect to the same session
- Auto-naming: Chat sessions are automatically named based on conversation content
- Smart Auto-focus: Input field auto-focuses on desktop after Claude responds (disabled on mobile to avoid keyboard popup)
- Voice Input: Tap the microphone button to dictate messages (uses Web Speech API, works on iOS Safari and Android Chrome)
- Dynamic Branding: Header displays the sprite's hostname with a neon green ๐พ
- Tailscale Integration: HTTPS access via Tailscale Serve, embedded in iframe from public URL
- Tailnet Gate: Public URL wakes sprite and embeds Tailscale URL in iframe (if authorized)
- Deep Linking: URL hash syncs bidirectionally between parent and iframe for shareable session links
- PWA Support: Installable as a Progressive Web App, works offline (requires HTTPS via Tailscale Serve)
- Auto-update: Pulls latest code when the service starts
- Sprite Network: Automatic discovery of other sprites in your Fly.io organization via shared Tigris bucket
- Network Restart: Run
scripts/restart-others.shto restart sprite-mobile on all other network sprites after pulling updates
Sprite Mobile uses Tailscale for secure access without passwords or tokens:
Public URL (https://sprite.sprites.app)
โ
โผ
Tailnet Gate (port 8080)
โ
โโโ Embed iframe with Tailscale HTTPS URL
โ โ
โ โโโ Iframe loads? โโโ Show sprite-mobile interface
โ โ (WebSocket keeps sprite awake)
โ โ
โ โโโ Iframe fails (4s timeout)? โโโ Show "Unauthorized" ๐พ ๐ซ
โ
โโโ Hash syncing โโโ Deep linking to specific sessions
Three access paths:
| Path | URL | Auth | HTTPS | PWA |
|---|---|---|---|---|
| Public | https://sprite.sprites.app |
Tailnet Gate | Yes | Via iframe |
| Tailscale Serve | https://my-sprite.ts.net |
Tailnet only | Yes | Yes |
| Tailscale IP | http://100.x.x.x:8081 |
Tailnet only | No | No |
Recommended: Bookmark the public URL. It wakes the sprite and embeds the Tailscale HTTPS URL in an iframe (with hash syncing for deep linking). A WebSocket keepalive keeps the sprite awake while the page is open.
If you prefer to set things up manually:
git clone <repo-url> sprite-mobile
cd sprite-mobile
bun install
bun startThe server runs on port 8081 by default. Override with the PORT environment variable.
Open http://localhost:8081 in a browser to access the chat interface.
All environment variables are managed through ~/.sprite-config, which serves as the single source of truth. Both bash and zsh automatically source this file.
Format:
# ~/.sprite-config
GH_TOKEN=ghp_xxxxx
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxx
FLY_API_TOKEN=fm2_xxxxx
SPRITE_API_TOKEN=your-org-name/org/id/token
SPRITE_PUBLIC_URL=https://my-sprite.sprites.app
TAILSCALE_SERVE_URL=https://my-sprite.tailxxxxx.ts.net
SPRITE_MOBILE_REPO=https://github.com/org/sprite-mobile| Variable | Description | Example |
|---|---|---|
PORT |
Server port | 8081 |
USE_GO_HUB |
Enable claude-hub for multi-client sync (default: true) |
true |
GO_HUB_URL |
WebSocket URL for claude-hub | ws://localhost:9090 |
SPRITE_PUBLIC_URL |
Public URL for waking sprite | https://my-sprite.sprites.app |
TAILSCALE_SERVE_URL |
Tailscale HTTPS URL | https://my-sprite.ts.net |
SPRITE_HOSTNAME |
Hostname for sprite network registration | my-sprite |
SPRITE_NETWORK_CREDS |
Path to Tigris credentials file | ~/.sprite-network/credentials.json |
SPRITE_NETWORK_ORG |
Fly.io org for sprite network | my-org |
GH_TOKEN |
GitHub Personal Access Token | ghp_xxxxx |
CLAUDE_CODE_OAUTH_TOKEN |
Claude Code OAuth token | sk-ant-oat01-xxxxx |
FLY_API_TOKEN |
Fly.io API token | fm2_xxxxx |
SPRITE_API_TOKEN |
Sprite CLI API token | your-org-name/org/id/token |
These are automatically configured by the setup script and stored in ~/.sprite-config.
sprite-mobile uses a multi-service architecture for reliable multi-client Claude Code session management:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Public Internet โ
โ https://my-sprite.sprites.app โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโผโโโโโโโโโโ
โ tailnet-gate โ Port 8080
โ (Public entry) โ โข Wakes sprite
โ โ โข Embeds Tailscale URL in iframe
โโโโโโโโโโโฌโโโโโโโโโโ โข WebSocket keepalive
โ
โโโโโโโโโโโผโโโโโโโโโโ
โ Tailscale HTTPS โ
โ my-sprite.ts.net โ
โโโโโโโโโโโฌโโโโโโโโโโ
โ
โโโโโโโโโโโผโโโโโโโโโโ
โ sprite-mobile โ Port 8081
โ (Web UI + API) โ โข Serves PWA interface
โ โ โข Proxies WebSocket to claude-hub
โโโโโโโโโโโฌโโโโโโโโโโ โข Maintains UI metadata
โ
โ WebSocket Proxy
โ
โโโโโโโโโโโผโโโโโโโโโโ
โ claude-hub โ Port 9090
โ (Session Mgr) โ โข Spawns/manages Claude processes
โ โ โข Multi-client sync
โ โ โข State machine (IDLE/WEB/TERMINAL)
โโโโโโโโโโโฌโโโโโโโโโโ โข Terminal session detection
โ
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโโผโโโโโโโ โโโโโผโโโโโ โโโโโโโผโโโโโโโ
โ Claude Process โ โ File โ โ Terminal โ
โ (headless) โ โWatcher โ โ Session โ
โโโโโโโโโโฌโโโโโโโโ โโโโโโโโโโ โโโโโโโฌโโโโโโโ
โ โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโผโโโโโโโโโโ
โ ~/.claude/ โ
โ projects/ โ
โ {cwd}/{uuid} โ
โ .jsonl โ
โ โ
โ (Source of Truth) โ
โโโโโโโโโโโโโโโโโโโโโ
Key architectural principles:
- Claude's
.jsonlfiles are the source of truth - All message history lives here - sprite-mobile is a proxy - Forwards WebSocket traffic to claude-hub
- claude-hub manages process lifecycle - State machine handles web/terminal transitions
- Multi-client sync - Multiple browsers and terminal sessions share the same Claude session
- No time-based cleanup - Sessions persist until explicitly terminated
After setup, these services run on your sprite:
| Service | Port | Description |
|---|---|---|
tailnet-gate |
8080 | Public entry point, embeds Tailscale URL in iframe with WebSocket keepalive |
sprite-mobile |
8081 | Main app server, proxies WebSocket connections to claude-hub |
claude-hub |
9090 | WebSocket hub for multi-client Claude Code session synchronization |
tailscaled |
- | Tailscale daemon |
Session data (source of truth):
~/.claude/projects/{cwdDir}/{sessionId}.jsonl- All message history in Claude's native format- This is the authoritative source for all conversation data
sprite-mobile lightweight metadata (data/ directory):
sessions.json- UI metadata only (session names, timestamps, preview text)sprites.json- Saved Sprite profiles for network discoveryuploads/{sessionId}/- Uploaded images per session
Architecture:
When USE_GO_HUB=true (the default), sprite-mobile acts as a WebSocket proxy to claude-hub. Messages flow:
Web Client โ sprite-mobile (proxy) โ claude-hub โ Claude process โ ~/.claude/projects/*.jsonl
Claude's .jsonl files are the source of truth. Sprite-mobile only maintains lightweight metadata for the UI (session list, previews, timestamps).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/config |
Get public configuration |
| GET | /api/sessions |
List all sessions |
| POST | /api/sessions |
Create new session |
| PATCH | /api/sessions/:id |
Update session name/cwd |
| DELETE | /api/sessions/:id |
Delete session |
| GET | /api/sessions/:id/messages |
Get message history |
| POST | /api/sessions/:id/regenerate-title |
Regenerate session title |
| POST | /api/upload?session={id} |
Upload an image |
| GET | /api/uploads/:sessionId/:filename |
Retrieve uploaded image |
| GET | /api/sprites |
List saved Sprite profiles |
| POST | /api/sprites |
Add a Sprite profile |
| PATCH | /api/sprites/:id |
Update a Sprite profile |
| DELETE | /api/sprites/:id |
Remove a Sprite profile |
| GET | /api/network/status |
Check if sprite network is configured |
| GET | /api/network/sprites |
Discover sprites in the network |
| POST | /api/network/heartbeat |
Manual heartbeat trigger |
| DELETE | /api/network/sprites/:hostname |
Remove a sprite from the network |
Connect to /ws?session={sessionId} to interact with a chat session.
With claude-hub (default):
- sprite-mobile acts as a transparent WebSocket proxy
- Messages flow:
Web Client โ sprite-mobile (proxy) โ claude-hub โ Claude process - Multiple clients can connect to the same session (synced in real-time)
- Terminal sessions and web clients share the same session seamlessly
Incoming messages (from claude-hub via proxy):
{ type: "system", subtype: "init", session_id: "..." }- Session initialized with Claude's UUID{ type: "history", messages: [...] }- Message history on connect{ type: "assistant", message: {...} }- Streaming assistant response{ type: "result", ... }- Response complete{ type: "user_message", message: {...} }- User message from another client{ type: "processing", isProcessing: true/false }- Processing state{ type: "system", message: "..." }- System notifications (e.g., "Switched to terminal mode")
Outgoing messages (to server):
{
"type": "user",
"content": "Your message here",
"imageId": "optional-image-id",
"imageFilename": "optional-filename",
"imageMediaType": "image/png"
}Session ID synchronization:
- Web clients start with a temporary UUID
- claude-hub spawns Claude, which generates its own session UUID
initmessage updates the frontend to use Claude's UUID- URL hash, session metadata, and
.jsonlfiles all sync to Claude's UUID
Two WebSocket endpoints keep the Sprite awake:
-
Public Gate Keepalive (
/keepaliveon port 8080): The tailnet-gate opens a WebSocket connection to the sprite's http_port (8080) to keep it awake while the public URL is open. This ensures the sprite doesn't suspend before the Tailscale connection is established. -
App Keepalive (
/ws/keepaliveon port 8081): The sprite-mobile app itself opens a WebSocket to keep the sprite awake while the app is in use.
Both use persistent WebSocket connections because sprites stay awake as long as there's an active connection to their http_port or any running service.
With claude-hub (default USE_GO_HUB=true):
- Creation: Browser connects to
ws://localhost:8081/ws?session={id} - Proxy: sprite-mobile proxies connection to
ws://localhost:9090/ws?session={id} - Session State Machine: claude-hub manages session state
IDLEโWEB_ONLY(first web client connects, spawns headless Claude process)WEB_ONLYโTERMINAL_ONLY(terminal session detected/exits)WEB_ONLY/TERMINAL_ONLYโIDLE(all clients disconnect)
- Process Lifecycle: Claude processes persist even after all clients disconnect
- No time-based cleanup or 30-minute timeouts
- Process stays alive until explicitly interrupted or sprite restarts
- Session files in
~/.claude/projects/preserve full history
- Reconnection: Resuming a session rejoins the existing process with full history
If you close your browser while Claude is working:
- WebSocket disconnects โ claude-hub keeps Claude process alive
- BUT sprite goes to sleep (no HTTP connections to port 8081)
- All processes freeze until sprite wakes up
Workarounds for long-running tasks:
- Keep browser tab open (even in background)
- Open terminal session:
sprite exec -s <sprite-name>keeps sprite awake
Sessions can specify a working directory (cwd) that Claude Code operates in. This defaults to the user's home directory.
sprite-mobile is designed as a personal tool for individual use, not for shared or public deployment. Each person should run their own instance(s) on their own Sprite(s). This significantly simplifies the security model:
- No multi-user authentication needed
- No per-user permissions or isolation
- Tailscale network membership IS the authentication
Access is controlled via Tailscale:
- Tailnet membership is the auth - No passwords or tokens needed
- Public URL embeds via iframe - The tailnet gate embeds the Tailscale URL in an iframe; if it fails to load within 4 seconds, shows "Unauthorized"
- Not on tailnet = Unauthorized - Users outside your tailnet see a blocked page with ๐พ ๐ซ
- Trust model: Anyone on your tailnet can use the app. Only add trusted devices/users to your tailnet.
This app runs Claude Code with --dangerously-skip-permissions, which allows Claude to execute commands without confirmation prompts. This is appropriate for:
- Personal use where you trust your own prompts
- A Sprite environment where the sandbox provides isolation
- "YOLO mode" vibe-coding workflows
Be aware that Claude has full access to the Sprite's filesystem and can run arbitrary commands. This is the intended behavior for a personal coding assistant.
If Chrome shows ERR_CERTIFICATE_TRANSPARENCY_REQUIRED when accessing the Tailscale URL:
- Wait a few minutes for certificate propagation
- Try hard refresh (Cmd+Shift+R)
- Clear site data in DevTools
- Try incognito mode
- Safari is more lenient and may work immediately
Check the serve status:
tailscale serve statusRestart if needed:
tailscale serve --bg 8081This project is licensed under the MIT License - see the LICENSE file for details.