Host mode
Host mode lets one OpenTodo process serve many independent tenants: a household, a team or a person, each with its own accounts and data. It is meant for a hosted service. A self-hosted server needs none of it, and without the variables below nothing changes.
On this page
Each tenant is an ordinary data directory, exactly like a self-hosted server's. It can be exported as a backup archive and restored into a self-hosted server with opentodo restore, and a self-hosted server's archive can become a tenant. Leaving the hosted service takes one download.
How it works#
- Routing. Tenant
smithofhttps://example.cloudanswers athttps://smith.example.cloud. A host name that names no tenant, or a deleted one, answers404and does not say whether it ever existed. A suspended tenant answers503. - Opened on demand. A tenant's database is opened on its first request, which also runs pending migrations, and closed after it has been idle (10 minutes unless changed). A device with a live update stream keeps it open. At most
OPENTODO_HOST_MAX_OPENtenants are open; at the cap the least recently used idle one closes first. - Failures stay local. When a tenant cannot be opened (for example a migration fails), that tenant answers
503, the error is shown to the operator (lastError), and every other tenant keeps working. The next attempt is made after a minute. - One scheduler. Reminders, scheduled backups, Trash retention, audit pruning, webhook deliveries, calendar subscriptions and weekly digests run from one scheduler. Each tenant records when its work is next due; the scheduler opens only tenants with due work, so thousands of idle tenants cost nothing. Reminders go out within the same 30 seconds as on a self-hosted server.
- Operator settings. Outgoing email (
OPENTODO_SMTP_*), single sign-on (OPENTODO_OIDC_*), backup destinations (OPENTODO_BACKUP_*), the MCP switches (OPENTODO_MCP,OPENTODO_MCP_WRITES), sign-up (OPENTODO_ALLOW_SIGNUP), attachment limits and the webhook network policy belong to the operator. Tenants cannot change them: their routes answer403 host_managed, and the app hides those panels. Email to task is off on a host. Each user still opts in to the weekly digest, sent through the operator's email. The assistant settings are read-only for tenant owners. Each tenant writes backups under its own name at the operator's destination. - Rolling updates. Several host processes may serve the same directory, as for one server: only one runs the scheduler, the other takes it over when it stops.
Configuration#
All variables are optional; OPENTODO_HOST_URL turns host mode on.
| Variable | Meaning |
|---|---|
OPENTODO_HOST_URL |
The address tenants live under, such as https://example.cloud. Point a wildcard DNS record and certificate (*.example.cloud) at the server |
OPENTODO_HOST_TOKEN |
The operator token, at least 32 characters. Required in host mode |
OPENTODO_HOST_OPERATOR_ADDR |
Where the operator interface listens (default 127.0.0.1:8081). Keep it on a private network |
OPENTODO_HOST_IDLE_MINUTES |
Minutes before an idle tenant closes (default 10) |
OPENTODO_HOST_MAX_OPEN |
Most tenants open at once (default 500) |
OPENTODO_HOST_LIMITS |
Default limits for every tenant, such as db_mb=500,ops_per_hour=5000,members=10 |
OPENTODO_DATA_DIR holds the registry (host.db) and one directory per tenant under tenants/. Use an empty directory: host mode refuses one that holds a self-hosted server's database. OPENTODO_BASE_URL is ignored, and host mode cannot be combined with OPENTODO_DEMO. The container health check answers on the bare address.
Limits#
| Key | Limit | When reached |
|---|---|---|
db_mb |
Database size in MB | New changes are refused with limit_storage; moving things to the Trash still works |
ops_per_hour |
Changes stored per hour | Refused with limit_ops_hour until the hour has passed |
ops_total |
Changes stored in total | Refused with limit_ops_total |
requests_per_minute |
Requests per minute | 429 limit_requests |
import_mb |
Size of an import file | 413 limit_import |
members |
Active accounts | New accounts are refused with 403 limit_members |
0 or a missing key means no limit. Changes are refused one at a time: the rest of a device's changes still sync, the refused ones stay on the device, marked as waiting, and are sent again later. Reads always work.
Operator interface#
Every request needs Authorization: Bearer <OPENTODO_HOST_TOKEN>; anything else answers 401 and names no tenant. It is never reachable through a tenant's host name.
| Method and path | What it does |
|---|---|
GET /operator/v1/tenants |
Every tenant with its state, limits, open state and last error |
POST /operator/v1/tenants |
{"name": "smith", "limits": {...}} creates an empty tenant. The answer carries setupToken and setupUrl: the first account needs the token, and the URL fills it in |
PUT /operator/v1/tenants/{name}/archive |
Creates a tenant from a backup archive (the request body). Send an encrypted archive's passphrase in X-Archive-Passphrase; without it the answer is 422 passphrase_required |
GET /operator/v1/tenants/{name} |
One tenant |
PATCH /operator/v1/tenants/{name} |
{"limits": {...}} replaces the tenant's limits |
POST /operator/v1/tenants/{name}/suspend |
Suspends it: 503 with its data kept |
POST /operator/v1/tenants/{name}/resume |
Resumes a suspended tenant |
GET /operator/v1/tenants/{name}/archive |
Downloads the tenant as a backup archive |
DELETE /operator/v1/tenants/{name} |
Removes its data directory. The name stays reserved for 30 days |
Names are 1 to 63 lowercase letters, digits and inner hyphens; common service names such as www, api and mail are reserved.
Owners take their data away#
On a hosted server the owner finds Settings → Server → Download your data: the whole server as one backup archive. To move to a self-hosted server, restore it there with opentodo restore (see Backups), start the server, sign in with the same credentials and point the devices at the new address.