Self-Hosting the Relay
Deploy your own TermPod relay on Cloudflare's free tier — setup, secrets, custom domains, and troubleshooting.
TermPod's relay server runs on Cloudflare Workers + Durable Objects. You can self-host it on Cloudflare's free tier with all features unlocked.
What the relay does
- Auth: Signup, login, token refresh, password reset
- Device management: Registration, heartbeat, session tracking
- WebSocket routing: Forwards encrypted terminal data between desktop and mobile
- WebRTC signaling: Exchanges offers/answers/ICE candidates for P2P connections
The relay is zero-knowledge — all terminal data is E2E encrypted. Self-hosted relays bypass all plan gates — all features are unlocked for all users.
Prerequisites
- Cloudflare account (free tier works)
- Node.js 20+
- pnpm 10+
Setup
1. Clone and install
git clone https://github.com/termpod/termpod.git
cd termpod
pnpm install2. Configure wrangler
The relay config lives in relay/wrangler.toml. The defaults work out of the box. If you want a custom worker name:
name = "my-termpod-relay"3. Set secrets
cd relay
# Required — JWT signing key
wrangler secret put JWT_SECRET
# Generate: openssl rand -base64 32
# Required for password reset emails
wrangler secret put RESEND_API_KEY
# Get from https://resend.com → API Keys
# Required for password reset — your sending address
wrangler secret put EMAIL_FROM
# Format: "YourApp <noreply@email.yourdomain.com>"
# You must verify this domain in Resend first4. Deploy
wrangler deployWrangler will output your relay URL, e.g. https://my-termpod-relay.your-account.workers.dev.
5. Point clients to your relay
Desktop: Set in .env before building:
VITE_RELAY_URL=wss://my-termpod-relay.your-account.workers.deviOS: Set in .env then regenerate config:
VITE_RELAY_URL=wss://my-termpod-relay.your-account.workers.dev
pnpm ios:generateThe desktop app also supports changing the relay URL at runtime in Settings → Connection.
Optional secrets
# Error tracking (Sentry)
wrangler secret put SENTRY_DSN
# WebRTC TURN relay (for P2P across restrictive NATs)
# Create at: https://dash.cloudflare.com → Calls → TURN Keys
wrangler secret put TURN_KEY_ID
wrangler secret put TURN_KEY_API_TOKENCustom domain
To use your own domain instead of *.workers.dev:
- Add the domain to your Cloudflare account
- In the Cloudflare dashboard, go to Workers & Pages → your worker → Settings → Domains & Routes
- Add a custom domain (e.g.
relay.yourdomain.com) - Update
VITE_RELAY_URLin your client configs towss://relay.yourdomain.com
Email setup (Resend)
Password reset requires a verified sending domain in Resend:
- Sign up at resend.com
- Add your sending domain (e.g.
email.yourdomain.com) — use a subdomain to protect your root domain's reputation - Add the DNS records Resend provides (SPF, DKIM, DMARC)
- Create an API key and set it via
wrangler secret put RESEND_API_KEY - Set your sending address via
wrangler secret put EMAIL_FROM
If RESEND_API_KEY is not configured, password reset returns 503. Everything else works normally.
Cloudflare free tier limits
| Resource | Free Tier Limit | Typical TermPod Usage |
|---|---|---|
| Worker requests | 100K/day | ~1K/day per active user |
| Durable Objects | Included | 1 User DO per account |
| DO storage (SQLite) | 1 GB | ~1 KB per user |
| DO WebSocket msgs | Included (hibernation) | Only billed when data flows |
| Bandwidth | Unlimited | Terminal data is small |
For most personal and small-team use, you will never exceed the free tier.
Updating
git pull
cd relay
pnpm deployDurable Object migrations are applied automatically. No manual database steps required.
Troubleshooting
"Email service not configured" on password reset: Set RESEND_API_KEY and EMAIL_FROM secrets.
WebRTC not working across networks: Set TURN_KEY_ID and TURN_KEY_API_TOKEN. Without TURN, WebRTC only works via STUN (fails on symmetric NATs). The relay WebSocket is always available as a fallback.
Clients can't connect: Verify the relay URL uses wss:// (not https://) in client configs. Check that the worker is deployed and the custom domain (if any) has valid DNS.