Skip to content

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.x through 172.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:

  1. Reverse proxy (nginx, caddy, traefik) terminates TLS.
  2. Cloud server container/process listens on private interface.
  3. Persistent volume stores MINDWTR_CLOUD_DATA_DIR.
  4. 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_TOKENS set to one or more strong tokens.
  • MINDWTR_CLOUD_CORS_ORIGIN set to your exact client origin.
  • MINDWTR_CLOUD_DATA_DIR mounted to persistent storage.
  • MINDWTR_CLOUD_MAX_BODY_BYTES and MINDWTR_CLOUD_MAX_ATTACHMENT_BYTES tuned for your usage.

Optional but useful:

  • MINDWTR_CLOUD_RATE_WINDOW_MS
  • MINDWTR_CLOUD_RATE_MAX
  • MINDWTR_CLOUD_ATTACHMENT_RATE_MAX

Environment Variables

Authentication

VariablePurposeNotes
MINDWTR_CLOUD_AUTH_TOKENSComma-separated allowlist of bearer tokens.Recommended setting for production.
MINDWTR_CLOUD_AUTH_TOKENS_FILEPath to a file containing bearer tokens.Useful for Docker secrets; file contents may match MINDWTR_CLOUD_AUTH_TOKENS.
MINDWTR_CLOUD_TOKENLegacy single-token alias.Still supported for backward compatibility, but deprecated.
MINDWTR_CLOUD_TOKEN_FILEPath to a file containing the legacy single token.Still supported for backward compatibility, but deprecated.
MINDWTR_CLOUD_ALLOW_ANY_TOKENAllows any syntactically valid bearer token.Explicit opt-in only. Best avoided outside controlled environments.
MINDWTR_CLOUD_ANY_TOKEN_MAX_NAMESPACESMaximum 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

VariablePurposeDefault
MINDWTR_CLOUD_CORS_ORIGINAllowed browser origin for CORS.http://localhost:5173 in non-production
MINDWTR_CLOUD_DATA_DIRDirectory for JSON namespaces, attachments, and locks../data
MINDWTR_CLOUD_TRUST_PROXY_HEADERSTrust X-Forwarded-For/proxy IP headers for auth-failure rate limiting.false
MINDWTR_CLOUD_TRUSTED_PROXY_IPSComma-separated proxy IP allowlist used when proxy headers are trusted.Empty; forwarded IPs are ignored unless the direct peer is trusted.

Request limits

VariablePurposeDefault
MINDWTR_CLOUD_MAX_BODY_BYTESMax JSON request size.2000000
MINDWTR_CLOUD_MAX_ATTACHMENT_BYTESMax attachment upload size.50000000
MINDWTR_CLOUD_REQUEST_TIMEOUT_MSPer-request timeout for cloud handlers.30000
MINDWTR_CLOUD_MAX_TASK_TITLE_LENGTHMax task title length accepted by cloud task endpoints.500
MINDWTR_CLOUD_MAX_TASK_QUICK_ADD_LENGTHMax quick-add input length accepted by cloud task creation.2000
MINDWTR_CLOUD_MAX_ITEMS_PER_COLLECTIONMax tasks/projects/sections/areas per uploaded collection.50000

Pagination and list shaping

VariablePurposeDefault
MINDWTR_CLOUD_LIST_DEFAULT_LIMITDefault page size for list endpoints.200
MINDWTR_CLOUD_LIST_MAX_LIMITHard cap for list endpoint page size.1000

Rate limiting

VariablePurposeDefault
MINDWTR_CLOUD_RATE_WINDOW_MSMain rate-limit window length.60000
MINDWTR_CLOUD_RATE_MAXMax non-attachment requests per window.120
MINDWTR_CLOUD_ATTACHMENT_RATE_MAXMax attachment requests per window.same as MINDWTR_CLOUD_RATE_MAX
MINDWTR_CLOUD_RATE_CLEANUP_MSInterval for pruning expired in-memory rate-limit entries.60000
MINDWTR_CLOUD_RATE_MAX_KEYSMax distinct in-memory rate-limit keys to keep before LRU-style eviction.10000
MINDWTR_CLOUD_AUTH_FAILURE_RATE_MAXMax unauthorized attempts per client IP/window before throttling.30

Operational guidance:

  • Keep proxy body limits aligned with MINDWTR_CLOUD_MAX_BODY_BYTES and MINDWTR_CLOUD_MAX_ATTACHMENT_BYTES.
  • Leave MINDWTR_CLOUD_TRUST_PROXY_HEADERS=false unless the server is only reachable through your reverse proxy. If you enable it, set MINDWTR_CLOUD_TRUSTED_PROXY_IPS to the proxy addresses that are allowed to supply forwarded client IPs.
  • If you rotate from MINDWTR_CLOUD_TOKEN to MINDWTR_CLOUD_AUTH_TOKENS, remember that token changes also change the namespace key.
  • Avoid MINDWTR_CLOUD_ALLOW_ANY_TOKEN=true for public deployments. It is capped by MINDWTR_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:

bash
cp docker/.env.https.example docker/.env.https.local

Edit docker/.env.https.local:

dotenv
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.https

Start the stack:

bash
docker compose --env-file docker/.env.https.local -f docker/compose.https.yaml up -d

Set 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:

dotenv
MINDWTR_CLOUD_DOMAIN=mindwtr.home.arpa
MINDWTR_CLOUD_CORS_ORIGIN=https://mindwtr.home.arpa
MINDWTR_CADDYFILE=Caddyfile.local-https

Every 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:

bash
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.crt

Install that certificate as a trusted root on each device that will sync to this hostname.

Minimal cloud service shape:

yaml
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-stopped

Operational notes:

  • The repository Dockerfile uses a multi-stage runtime image and pins the Bun base image by digest for repeatable rebuilds.
  • Mount /data on durable disk, not ephemeral container FS.
  • Keep tokens in secrets manager or .env outside git.
  • For Docker secrets, use MINDWTR_CLOUD_AUTH_TOKENS_FILE instead 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 Authorization header unchanged.
  • Set request timeout high enough for large attachment uploads.
  • Restrict access by IP/VPN if possible.

Example Caddyfile:

caddyfile
mindwtr.example.com {
  reverse_proxy mindwtr-cloud:8787
}

For LAN-only internal certificates:

caddyfile
mindwtr.home.arpa {
  tls internal
  reverse_proxy mindwtr-cloud:8787
}

Example nginx snippets:

nginx
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:

  1. Snapshot or archive MINDWTR_CLOUD_DATA_DIR.
  2. Keep point-in-time backups (daily + weekly retention).
  3. Verify restore periodically.

Restore:

  1. Stop server.
  2. Restore directory contents to MINDWTR_CLOUD_DATA_DIR.
  3. Start server.
  4. Check GET /health and 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:

text
POST /v1/attachments/orphans
DELETE /v1/attachments/orphans

Run 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:

  1. Take backup.
  2. Deploy new version in staging or canary first.
  3. Run smoke checks:
    • GET /health
    • authenticated GET /v1/data
    • authenticated GET /v1/tasks
    • authenticated GET /v1/projects, GET /v1/areas, and GET /v1/sections
    • small and large attachment upload/download
  4. Deploy to production.
  5. Monitor logs for rate limit, invalid payload, and permission denied errors.

Token Rotation

Recommended rotation flow:

  1. Add new token to MINDWTR_CLOUD_AUTH_TOKENS alongside old token.
  2. Update clients to new token.
  3. 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 writable
  • Invalid 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.

Mindwtr is free, open source, and local-first.