Docs Your data
Run your own studio
Your studio runs in your own cloud account, with your own keys. Steward the company never gets a copy. One deployment is one studio.
You need three things: the Steward repository, a place to run one small server with a disk, and a web address for it.
What every host needs
- A volume at
/data. All studio data is one SQLite file,/data/steward.db. Without a volume, a restart wipes the studio. - One copy of the server. SQLite lives on that one volume, so don't scale to two.
PUBLIC_URL. The address people open, likehttps://studio.example.com. Invite links and public pages use it.SETUP_TOKEN(recommended). A long random string you make up. You enter it once at/setupto create the first owner. Without it, the server makes one and prints it in the log.
The health check is GET /health. The container listens on PORT (hosts set it for you).
Railway
- Make a new project from the repository (or from the Steward template, once it's published).
- In the service's Settings, set Config as code to
deploy/railway.json. It builds the Dockerfile, checks/healthand keeps one replica. - Add a volume to the service, mounted at
/data. - Variables:
PUBLIC_URL(your Railway domain or your own) andSETUP_TOKEN. - Deploy. Open
PUBLIC_URL/setup.
Template variables, if you publish this as a Railway template: PUBLIC_URL (required, default https://${{RAILWAY_PUBLIC_DOMAIN}}), SETUP_TOKEN (required, generated with ${{secret(32)}}), and a volume at /data.
Fly.io
fly.toml is in the repository.
fly launch --copy-config --no-deploy # choose your own app name fly volumes create steward_data --size 1 fly secrets set PUBLIC_URL=https://<app>.fly.dev SETUP_TOKEN=<a long random string> fly deploy
Open https://<app>.fly.dev/setup. The config keeps one machine running all the time, since the schedule runs every minute.
Any server with Docker
docker-compose.yml is in the repository.
cp deploy/env.example .env # fill in PUBLIC_URL and SETUP_TOKEN docker compose up -d --build docker compose logs steward
Put HTTPS in front of port 3800 with Caddy, nginx or a tunnel. With Caddy, two lines are enough:
studio.example.com { reverse_proxy localhost:3800 }
The first run
Open PUBLIC_URL/setup. Enter the setup token, the studio's name, your handle, your name and a passcode. You're signed in as the first owner, and setup closes for good. Invite everyone else from Settings, then People.
The token works once. If the server made it, it's in the log, and each restart before setup makes a new one.
SEED_PEOPLE still works instead of /setup: SEED_PEOPLE=ana:Ana,ben:Ben creates those people on the first boot and prints an invite link for each.
Where each optional key goes
All of these go in the host's variables (Railway Variables, fly secrets set, or .env). None is needed to start. deploy/env.example lists them with notes.
| What | Variables | Without them |
|---|---|---|
| @steward on an API key | STEWARD_BRAIN=api and ANTHROPIC_API_KEY or another provider's key (models) | @steward thinks through an owner's connector |
| Nightly backups to your bucket | BACKUP_S3_BUCKET, BACKUP_S3_ACCESS_KEY_ID, BACKUP_S3_SECRET_ACCESS_KEY, plus BACKUP_S3_ENDPOINT for R2, B2 or MinIO; optional BACKUP_S3_REGION, BACKUP_S3_PREFIX (steward/), BACKUP_RETENTION_DAYS (30), BACKUP_HOUR_UTC (10) | Off. You can still download everything from Settings |
| Update notices | UPDATE_FEED_URL, a public JSON file like { "latest": "0.7.0", "url": "https://...", "notes": "..." } | Off |
| Plan and billing reports | BILLING_URL, BILLING_CUSTOMER_ID, BILLING_KEY, from your Steward account | Off, and nothing is sent even with them until an owner opts in |
| Web push | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT | Off |
| iPhone push | APNS_KEY_ID, APNS_TEAM_ID, APNS_KEY | Off |
| GitHub, Railway, email, Bluesky, Stripe | See Integrations and trust | Each is off |
| Time zone for the schedule | STUDIO_TZ (default America/Los_Angeles) | Pacific time |
| Behind a proxy (rate limits by visitor address) | TRUST_PROXY=1 (detected on Railway and Fly) | The socket's address is used, and forwarded headers are ignored |
| The key that seals two-step secrets | STEWARD_SECRET_KEY | A key file the hub makes once, DATA_DIR/secret.key |
| Checks that may reach your own network | ALLOW_PRIVATE_FETCH=1 | Uptime, health and site checks reach only the public internet |
| More push services | PUSH_EXTRA_HOSTS (hosts, comma-separated) | Only the browsers' own push services |
| Passkeys that survive a move to a subdomain | PASSKEY_RP_ID, a parent domain of PUBLIC_URL's host (like example.com for app.example.com) | Passkeys belong to PUBLIC_URL's host, and a new address needs new passkeys |
| A public site on another domain (how stewardstudio.io runs) | SITE_HOSTS (hosts that get the landing page and public docs, comma-separated; default stewardstudio.io,www.stewardstudio.io) and APP_URL (where those hosts send app pages) | Your hub's own address serves the app only |
Back up and move
- Download: Settings, then Your data. The database file is a consistent copy made while the studio runs. The JSON has the ledger, businesses, charters, tasks, decisions, memory and numbers.
- Restore or move: stop the server, put the downloaded file in the new volume as
/data/steward.db, start it. Everyone signs in as before. - Nightly: with the backup variables set, a copy goes to your bucket every night as
steward/steward-YYYY-MM-DD.db, and copies older than the retention are deleted. Nothing else in the bucket is touched.
Updates
Settings, then Version shows the version you run. With UPDATE_FEED_URL set, it also says when a newer release is out. The server only reads that file; it sends nothing. To update, redeploy from the newer code. The database upgrades itself on start.