Cloud Deployment
This page covers how to run the apps/cloud server reliably in production-like self-hosted environments.
Scope
- Mindwtr Cloud is a lightweight self-hosted backend for JSON sync and token-authenticated task automation endpoints, not a full hosted app UI.
- It is best for single-tenant or small trusted deployments.
- You should run it behind HTTPS reverse proxying and standard server hardening controls.
Client compatibility note:
- Mindwtr Cloud clients require HTTPS for public URLs.
- HTTP is accepted only for local/private targets such as
localhost,127.0.0.1,10.x.x.x,172.16.x.xthrough172.31.x.x,192.168.x.x, loopback/private IPv6 addresses,*.local, and*.home.arpa. - For custom DNS, VPN, Tailscale, ZeroTier, or other names that are not recognized as local/private, add TLS at the reverse proxy layer.
- The Allow insecure connections (HTTP) setting is for trusted local/private endpoints only; it is not a public HTTP override.
Deployment Topology
Recommended layout:
- Reverse proxy (
nginx,caddy,traefik) terminates TLS. - Cloud server container/process listens on private interface.
- Persistent volume stores
MINDWTR_CLOUD_DATA_DIR. - Regular backups snapshot the data directory.
The same cloud service handles both:
- Sync traffic under
/v1/data - Task automation endpoints such as
/v1/tasks,/v1/projects,/v1/areas,/v1/sections, and/v1/search
PUT /v1/data is merge-based, not a blind replacement. The server reads the current namespace snapshot, merges it with the uploaded snapshot using Mindwtr's normal revision-aware sync rules, validates the merged data, and then writes it back. A client that uploads an older or partial view should not expect to erase newer remote records simply by sending a full JSON payload.
REST reference fields must point to live records. For example, creating or patching a project with an areaId whose area was soft-deleted returns 404 Area not found rather than attaching the project to a tombstone. Use areaId: null to clear a project area; an empty string is rejected.
For endpoint-level request and response details, see Cloud API.
Environment Baseline
Minimum production baseline:
MINDWTR_CLOUD_AUTH_TOKENSset to one or more strong tokens.MINDWTR_CLOUD_CORS_ORIGINset to your exact client origin.MINDWTR_CLOUD_DATA_DIRmounted to persistent storage.MINDWTR_CLOUD_MAX_BODY_BYTESandMINDWTR_CLOUD_MAX_ATTACHMENT_BYTEStuned for your usage.
Optional but useful:
MINDWTR_CLOUD_RATE_WINDOW_MSMINDWTR_CLOUD_RATE_MAXMINDWTR_CLOUD_ATTACHMENT_RATE_MAX
Environment Variables
Authentication
| Variable | Purpose | Notes |
|---|---|---|
MINDWTR_CLOUD_AUTH_TOKENS | Comma-separated allowlist of bearer tokens. | Recommended setting for production. |
MINDWTR_CLOUD_AUTH_TOKENS_FILE | Path to a file containing bearer tokens. | Useful for Docker secrets; file contents may match MINDWTR_CLOUD_AUTH_TOKENS. |
MINDWTR_CLOUD_TOKEN | Legacy single-token alias. | Still supported for backward compatibility, but deprecated. |
MINDWTR_CLOUD_TOKEN_FILE | Path to a file containing the legacy single token. | Still supported for backward compatibility, but deprecated. |
MINDWTR_CLOUD_ALLOW_ANY_TOKEN | Allows any syntactically valid bearer token. | Explicit opt-in only. Best avoided outside controlled environments. |
MINDWTR_CLOUD_ANY_TOKEN_MAX_NAMESPACES | Maximum number of distinct namespaces that may be created when any-token mode is enabled. | Defaults to 32; set only for controlled automation environments. |
Networking and storage
| Variable | Purpose | Default |
|---|---|---|
MINDWTR_CLOUD_CORS_ORIGIN | Allowed browser origin for CORS. | http://localhost:5173 in non-production |
MINDWTR_CLOUD_DATA_DIR | Directory for JSON namespaces, attachments, and locks. | ./data |
MINDWTR_CLOUD_TRUST_PROXY_HEADERS | Trust X-Forwarded-For/proxy IP headers for auth-failure rate limiting. | false |
MINDWTR_CLOUD_TRUSTED_PROXY_IPS | Comma-separated proxy IP allowlist used when proxy headers are trusted. | Empty; forwarded IPs are ignored unless the direct peer is trusted. |
Request limits
| Variable | Purpose | Default |
|---|---|---|
MINDWTR_CLOUD_MAX_BODY_BYTES | Max JSON request size. | 2000000 |
MINDWTR_CLOUD_MAX_ATTACHMENT_BYTES | Max attachment upload size. | 50000000 |
MINDWTR_CLOUD_REQUEST_TIMEOUT_MS | Per-request timeout for cloud handlers. | 30000 |
MINDWTR_CLOUD_MAX_TASK_TITLE_LENGTH | Max task title length accepted by cloud task endpoints. | 500 |
MINDWTR_CLOUD_MAX_TASK_QUICK_ADD_LENGTH | Max quick-add input length accepted by cloud task creation. | 2000 |
MINDWTR_CLOUD_MAX_ITEMS_PER_COLLECTION | Max tasks/projects/sections/areas per uploaded collection. | 50000 |
Pagination and list shaping
| Variable | Purpose | Default |
|---|---|---|
MINDWTR_CLOUD_LIST_DEFAULT_LIMIT | Default page size for list endpoints. | 200 |
MINDWTR_CLOUD_LIST_MAX_LIMIT | Hard cap for list endpoint page size. | 1000 |
Rate limiting
| Variable | Purpose | Default |
|---|---|---|
MINDWTR_CLOUD_RATE_WINDOW_MS | Main rate-limit window length. | 60000 |
MINDWTR_CLOUD_RATE_MAX | Max non-attachment requests per window. | 120 |
MINDWTR_CLOUD_ATTACHMENT_RATE_MAX | Max attachment requests per window. | same as MINDWTR_CLOUD_RATE_MAX |
MINDWTR_CLOUD_RATE_CLEANUP_MS | Interval for pruning expired in-memory rate-limit entries. | 60000 |
MINDWTR_CLOUD_RATE_MAX_KEYS | Max distinct in-memory rate-limit keys to keep before LRU-style eviction. | 10000 |
MINDWTR_CLOUD_AUTH_FAILURE_RATE_MAX | Max unauthorized attempts per client IP/window before throttling. | 30 |
Operational guidance:
- Keep proxy body limits aligned with
MINDWTR_CLOUD_MAX_BODY_BYTESandMINDWTR_CLOUD_MAX_ATTACHMENT_BYTES. - Leave
MINDWTR_CLOUD_TRUST_PROXY_HEADERS=falseunless the server is only reachable through your reverse proxy. If you enable it, setMINDWTR_CLOUD_TRUSTED_PROXY_IPSto the proxy addresses that are allowed to supply forwarded client IPs. - If you rotate from
MINDWTR_CLOUD_TOKENtoMINDWTR_CLOUD_AUTH_TOKENS, remember that token changes also change the namespace key. - Avoid
MINDWTR_CLOUD_ALLOW_ANY_TOKEN=truefor public deployments. It is capped byMINDWTR_CLOUD_ANY_TOKEN_MAX_NAMESPACES, but fixed token allowlists are still the production model.
Docker Runbook
Start with Docker Deployment for the supported Compose entry points. This section is the operations checklist for running the same cloud container in production-like environments.
For a local HTTP-only smoke test, use docker/compose.yaml.
For public desktop or mobile client URLs, use the HTTPS stack:
cp docker/.env.https.example docker/.env.https.localEdit docker/.env.https.local:
MINDWTR_CLOUD_DOMAIN=mindwtr.example.com
MINDWTR_CLOUD_AUTH_TOKENS=your_long_random_token
MINDWTR_CLOUD_CORS_ORIGIN=https://mindwtr.example.com
MINDWTR_CADDYFILE=Caddyfile.httpsStart the stack:
docker compose --env-file docker/.env.https.local -f docker/compose.https.yaml up -dSet Mindwtr's Self-Hosted URL to the base URL, for example https://mindwtr.example.com. Mindwtr appends /v1/data automatically.
Use Caddyfile.local-https for LAN-only hostnames with Caddy's internal CA:
MINDWTR_CLOUD_DOMAIN=mindwtr.home.arpa
MINDWTR_CLOUD_CORS_ORIGIN=https://mindwtr.home.arpa
MINDWTR_CADDYFILE=Caddyfile.local-httpsEvery device must trust Caddy's local root certificate before a client will accept this certificate. Public certificates are usually simpler for mobile clients.
After the LAN-only stack starts, export the local root certificate:
docker compose --env-file docker/.env.https.local -f docker/compose.https.yaml cp caddy:/data/caddy/pki/authorities/local/root.crt ./mindwtr-caddy-root.crtInstall that certificate as a trusted root on each device that will sync to this hostname.
Minimal cloud service shape:
services:
mindwtr-cloud:
build:
context: .
dockerfile: docker/cloud/Dockerfile
environment:
MINDWTR_CLOUD_DATA_DIR: /data
MINDWTR_CLOUD_AUTH_TOKENS: ${MINDWTR_CLOUD_AUTH_TOKENS}
MINDWTR_CLOUD_CORS_ORIGIN: https://mindwtr.example.com
MINDWTR_CLOUD_RATE_MAX: "120"
MINDWTR_CLOUD_ATTACHMENT_RATE_MAX: "120"
volumes:
- ./mindwtr-cloud-data:/data
restart: unless-stoppedOperational notes:
- The repository Dockerfile uses a multi-stage runtime image and pins the Bun base image by digest for repeatable rebuilds.
- Mount
/dataon durable disk, not ephemeral container FS. - Keep tokens in secrets manager or
.envoutside git. - For Docker secrets, use
MINDWTR_CLOUD_AUTH_TOKENS_FILEinstead of inlining the token in compose. - The same deployed container serves both sync and REST API traffic on the same host/port.
Reverse Proxy Checklist
At proxy layer:
- Enforce HTTPS.
- Limit request body size to match cloud limits.
- Forward
Authorizationheader unchanged. - Set request timeout high enough for large attachment uploads.
- Restrict access by IP/VPN if possible.
Example Caddyfile:
mindwtr.example.com {
reverse_proxy mindwtr-cloud:8787
}For LAN-only internal certificates:
mindwtr.home.arpa {
tls internal
reverse_proxy mindwtr-cloud:8787
}Example nginx snippets:
client_max_body_size 50m;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_set_header Authorization $http_authorization;Backups and Restore
Data format is file-per-token JSON plus attachment files.
Backup:
- Snapshot or archive
MINDWTR_CLOUD_DATA_DIR. - Keep point-in-time backups (daily + weekly retention).
- Verify restore periodically.
Restore:
- Stop server.
- Restore directory contents to
MINDWTR_CLOUD_DATA_DIR. - Start server.
- Check
GET /healthand run a client sync validation.
Attachment Cleanup
When a user deletes an attachment, clients keep a pendingRemoteDeletes record until the backend delete succeeds. Those pending deletes are intentionally not aged out, because removing them before a successful remote delete can leave private files behind.
Mindwtr Cloud also provides authenticated orphan cleanup for attachment files that are no longer referenced by the current data.json snapshot:
POST /v1/attachments/orphans
DELETE /v1/attachments/orphansRun this after restore operations or as a periodic maintenance task if you want server-side cleanup of files that became unreachable outside the normal client delete flow. The endpoint scans the authenticated token namespace only and returns counts for scanned, kept, deleted, and failed file paths.
The cleanup skips attachment files modified in the last five minutes so an upload followed by a later /v1/data reference cannot be deleted by a concurrent maintenance run.
Upgrade Procedure
Safe rolling procedure:
- Take backup.
- Deploy new version in staging or canary first.
- Run smoke checks:
GET /health- authenticated
GET /v1/data - authenticated
GET /v1/tasks - authenticated
GET /v1/projects,GET /v1/areas, andGET /v1/sections - small and large attachment upload/download
- Deploy to production.
- Monitor logs for
rate limit,invalid payload, andpermission deniederrors.
Token Rotation
Recommended rotation flow:
- Add new token to
MINDWTR_CLOUD_AUTH_TOKENSalongside old token. - Update clients to new token.
- Remove old token after migration window.
Because token hash maps namespace/file, changing token changes storage namespace. If you require continuity under a new token, migrate corresponding data file/attachment directory deliberately.
Observability
The cloud server writes structured JSON logs to stdout/stderr.
Minimum log alerts:
- Repeated
Unauthorized - Frequent
Rate limit exceeded Cloud data directory is not writableInvalid remote sync payload
Add host/container metrics:
- CPU and memory
- disk free space on data volume
- p95 request latency
- non-2xx response rate
Clock note:
- The server participates in merge and repair on
PUT /v1/data, so host clock drift can still affect request logs and rate-limit windows. Keep NTP or equivalent time sync enabled. - Merge repair timestamps use the server wall clock. This prevents a client clock that is a few minutes fast from poisoning server-generated repair metadata.
Failure Modes
- Permission errors: volume ownership/permissions mismatch.
- CORS failures: wrong
MINDWTR_CLOUD_CORS_ORIGIN. - Token mismatch: client token not in allowlist.
- Large payload failures: body limits exceeded at proxy or app layer.