Self-hosting
Managed services
Instead of running everything on one server, a deployment can use hosted services: nothing to patch or back up yourself, and each can grow on its own. This page goes through each service, then running the app itself on Fly.io, as tabsandspaces.app does.
Put the services close together: the same cloud region (for example London, Frankfurt or US East) keeps the app fast.
MongoDB (Atlas)
The app keeps the deployment's data in one database, and gives each workspace a database of its own, which it creates, and deletes when the workspace is deleted or moves.
- Create a cluster in the region you chose. The free tier (512 MB, up to 100 databases) is fine to try it; use a paid tier for real use.
- Create a database user that may read, write, create and delete databases. Atlas's "Read and write to any database" role can't delete a database, so make a custom role with
readWriteAnyDatabaseand the actiondropDatabaseon all databases, and give the user that role. - Network access: allow your app's outgoing addresses. If your host has no fixed addresses (Fly.io without static egress IPs, for example), allow all (
0.0.0.0/0) and rely on the user's long password until you can narrow it. - Connection string: from Connect → Drivers, with the password filled in and the database name added after
.mongodb.net/:
MONGODB_URI=mongodb+srv://tabs-and-spaces:PASSWORD@cluster.abc123.mongodb.net/tabs-and-spaces?retryWrites=true&w=majorityAtlas clusters are replica sets, so the app uses transactions (its logs say transactions: on).
Redis
The job queue (schedules, agents' work, emails) needs Redis 6.2 or later, or Valkey, with eviction turned off: a Redis that evicts when it's full silently drops queued jobs.
| Provider | Notes |
|---|---|
| Upstash through Fly.io | flyctl redis create --region lhr --no-replicas --disable-eviction, pay as you go. Reachable only on Fly's private network |
| Upstash, Redis Cloud, ElastiCache… | Choose "no eviction" (noeviction), and use rediss:// (TLS) when it's reached over the internet |
REDIS_URL=rediss://default:PASSWORD@your-redis.example.net:6379Give each deployment a Redis database of its own: two deployments sharing one would run each other's jobs. It needs no backups: the app rebuilds pending work from MongoDB.
File storage (S3 or R2)
Any S3-compatible storage works. With Cloudflare R2 (no charge for downloads):
- Create a bucket (for example
tabs-and-spaces-files) in the location nearest your app. - R2 → Manage API tokens → Create API token: permission Object Read & Write, limited to that bucket. Copy the Access Key ID and Secret Access Key (shown once).
S3_ENDPOINT=https://<account id>.r2.cloudflarestorage.com
S3_REGION=auto
S3_BUCKET=tabs-and-spaces-files
S3_FORCE_PATH_STYLE=true
S3_ACCESS_KEY_ID=…
S3_SECRET_ACCESS_KEY=…For AWS S3, leave S3_ENDPOINT empty and set S3_REGION to the bucket's region. For MinIO or SeaweedFS, set the endpoint and S3_FORCE_PATH_STYLE=true.
The app sends email over SMTP. With Resend:
- Domains → Add Domain: your domain, in the region nearest your app. Add the DNS records it lists (an MX and TXT records for sending, and DKIM), then Verify.
- API Keys → Create API Key with Sending access for that domain. The key is the SMTP password.
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=resend
SMTP_PASSWORD=re_…
MAIL_FROM=Your Team <no-reply@your-domain.com>Postmark, Amazon SES, Mailgun and others work the same way, with their SMTP settings.
Running on Fly.io
The repository's deploy/fly/ has a config for each app:
| Config | App | Reached at |
|---|---|---|
api.toml | The API: always on (it runs the job queue) | Only Fly's private network: the web app forwards /api to it |
web.toml | The web server: the app, forwarding /api | Your domain |
control.toml | Our control centre (you don't need it) |
Change the app names, region, PUBLIC_URL, CORS_ORIGINS, and the S3_* and SMTP_* settings in api.toml and web.toml to yours. The web app reaches the API at <api app>.internal:4400 (API_UPSTREAM in web.toml).
1. Create the apps
flyctl auth login
flyctl apps create my-tabs-api --org my-org
flyctl apps create my-tabs-web --org my-org2. Set the API's secrets
deploy/fly/secrets.sh api reads them from a private env file (~/.tabs-and-spaces/production.env by default) and sets them on Fly without printing them. Or set them yourself:
flyctl secrets set -a my-tabs-api --stage \
MONGODB_URI=… REDIS_URL=… S3_ACCESS_KEY_ID=… S3_SECRET_ACCESS_KEY=… \
SMTP_PASSWORD=… AUTH_SECRET=$(openssl rand -base64 48)AUTH_SECRET signs sessions and encrypts saved credentials: keep a copy somewhere safe (see Backups).
3. Deploy
From the repository root (Fly builds the images on its own servers):
flyctl deploy . --config deploy/fly/api.toml --ha=false
flyctl deploy . --config deploy/fly/web.toml --ha=falseThe API's logs then show listening and the first-run setup link.
4. Your domain
flyctl certs add chat.example.com -a my-tabs-webIt prints the DNS records to add: an A and an AAAA record for the domain, and a CNAME for _acme-challenge, which lets Fly prove the domain is yours. If your DNS is on Cloudflare, set these records to DNS only (grey cloud): Fly handles HTTPS itself. flyctl certs check chat.example.com -a my-tabs-web shows when the certificate is issued, usually within minutes.
Then open the first-run setup link.