EMStool LLC — Cadence MIH Scheduling Platform

Client Deployment Configuration Guide

Policy #: HIPAA-SEC-015 Effective Date: July 16, 2026 Version: 1.3 Next Review: July 16, 2027

1. Purpose & Scope

This describes a client's deployment — not EMStool's own infrastructure

Each client runs Cadence on its own dedicated hardware — an EMStool-provided appliance or the client's own server, in the client's own facility — fully isolated from every other client and from EMStool's own systems. This document is generic by design: it describes what any client's deployment contains, not a specific instance's IP addresses or container names. EMStool's own operational infrastructure (license server, its own demo/test instances, monitoring tooling, marketing site) is documented separately in the EMStool Internal Infrastructure Runbook (internal only) (HIPAA-SEC-014) and is not installed on client hardware.

2. What a Client Deployment Contains

A single client deployment is exactly two containers plus a tunnel — nothing else from EMStool's own stack ships with it:

ComponentImagePurposeEnv Vars (names only)
Databasemariadb:latestThe client's own isolated Cadence database — one per deployment, never shared. MARIADB_DATABASE, MARIADB_USER, MARIADB_PASSWORD, MARIADB_ROOT_PASSWORD
App / Web Serverlscr.io/linuxserver/swag:latestReverse proxy + TLS termination + the Cadence PHP application itself. EMAIL, PUID, PGID, TZ, URL, SUBDOMAINS, ONLY_SUBDOMAINS, VALIDATION, DNSPLUGIN, DHLEVEL
TunnelCloudflare Tunnel connectorSole external ingress path — no inbound port is ever opened on the client's network. Client-owned by default (see Section 4); EMStool can optionally manage it on request. TUNNEL_TOKEN, TUNNEL_RETRIES, TUNNEL_TRANSPORT_PROTOCOL

Application-level configuration is supplied via config/.env (see config/.env.example in the mih-scheduler repo for the full, current template) — database credentials, the app URL, and optional integrations below.

3. Optional Integrations (client's own choice, per deployment)

IntegrationPurposeDefault if unconfigured
OSRM (route optimization)Self-hosted routing engine for the Route Optimizer feature.Disabled — never falls back to a public OSRM instance, since that would send patient address coordinates outside the deployment.
Nominatim (geocoding)Self-hosted address → coordinate geocoding.Disabled — never falls back to the public nominatim.openstreetmap.org, for the same reason.
Resend (transactional email)Password reset, welcome emails, video-consult invites.Disabled — email sending simply doesn't happen unconfigured.
Backup destinationWhere encrypted nightly backups are uploaded.The client's own choice — Backblaze B2, a local NAS via rsync, or any destination the client controls. See the Vendor Security Register (HIPAA-SEC-012) for the vendor-BAA implications of choosing Backblaze.
Uptime/host monitoringOptional self-hosted Uptime Kuma / Netdata, same pattern EMStool uses for its own instances.Disabled — a client may stand up their own copy if desired; not part of the base deployment.

PDF/document rendering via Gotenberg was removed from the application entirely on 2026-07-16 — all report and chart exports now use the browser's own print-to-PDF, so no rendering service is needed in a client deployment at all.

4. Tunnel Ownership

The Cloudflare Tunnel connecting a client's deployment to the internet is, by default, the client's own Cloudflare account — the client owns that account and any BAA obligation tied to it. If a client prefers EMStool to set up and manage the tunnel on their behalf, EMStool can do so as a service; in that case the tunnel account (and its BAA, if applicable) is EMStool's own.

4a. Power Redundancy

EMStool's own physical server(s) run in an office environment with UPS and generator backup power. We recommend any client running Cadence on their own hardware have similar power redundancy for their server — a UPS at minimum, a generator where feasible — since an unplanned power loss on client-owned hardware is outside EMStool's ability to prevent or mitigate.

5. Backup & Recovery

Recovery order for a client deployment mirrors the EMStool-internal runbook's Section 5, scoped down to this deployment's two containers: (1) provision replacement hardware, (2) recreate the MariaDB and SWAG containers with the same config/.env values, (3) restore the database from the client's configured backup destination, (4) clone the mih-scheduler repo and run ./deploy.sh migrate, (5) point the Cloudflare Tunnel at the new host.

5a. Local/rsync Backup vs. Backblaze B2

Backblaze B2Local + rsync
Who moves the file offsiteThe app itself, automatically, right after each backup completesThe client's own external rsync/cron job, on a separate machine, pulling on its own schedule
Truly offsite?Yes, always — B2 is a different physical location by definitionOnly if the client's rsync destination is a genuinely separate machine/location. If nothing ever copies the file off the Cadence host, it's not offsite at all — same building, same disaster risk (fire, theft, hardware failure) as the primary server.
Ongoing costBackblaze storage fees (billed to whoever owns that B2 account — see Section 4, Tunnel Ownership, for the same client-vs-EMStool ownership split)No cloud storage cost — just whatever storage the client already owns at the destination
Setup effortOne-time: create a B2 account, enter 3 credentials in Admin settingsClient sets up and maintains their own rsync/cron job (Section 5b below) — nothing to configure inside Cadence itself
EncryptionSame — every backup is GPG-encrypted before either path ever touches itSame

5b. Setting Up Local Backup (No Backblaze)

If Backblaze B2 isn't configured (the b2_key_id/b2_app_key/b2_bucket_name settings are left blank), Cadence still writes a full, GPG-encrypted backup on every scheduled run — it just stays on local disk instead of also being uploaded. Getting that file off the box (to another server, a NAS, external drive, etc.) is the client's own responsibility, done outside the app with standard tools.

Where the file lands

Each backup is written inside the SWAG/app container at the path set by the backup_path app setting (default /config/backups), named scheduler_backup_YYYY-MM-DD_H-i-s.sql.gpg. That container path is backed by a Docker volume mount on the host — whatever host directory was mapped to the container's /config when the deployment was provisioned is where the real files live on disk. Ask whoever set up the deployment for that exact host path if it isn't already documented for your install.

Retention is already automatic

The app itself deletes its own older backups once there are more than backup_retention (default 7) present in that directory — this happens regardless of whether Backblaze is configured. An external rsync job doesn't need to manage cleanup on the source side; it only needs to copy whatever's currently there.

Example: a nightly rsync pull to another machine

Run this from the machine you want the backups copied to (not from the Cadence host itself), as a cron job:

0 5 * * * rsync -az --delete-after user@cadence-host:/path/to/appdata/swag-instance/backups/ /local/backup/archive/cadence/

Replace user@cadence-host and the source path with your deployment's actual SSH access and the host-side backup directory identified above. --delete-after keeps the destination mirrored to whatever the app currently has (relying on the app's own retention, not a second independent retention policy) — drop that flag if you'd rather keep every backup forever on the destination side regardless of what the source has rotated out.

Each .sql.gpg file is already encrypted before it ever touches disk — copying it over plain rsync/SSH doesn't re-expose the data; the GPG passphrase (set via BACKUP_ENCRYPTION_PASSPHRASE in config/.env) is what's needed to actually restore from any of these files later.

6. Provisioning

New client deployments are set up using EMStool's internal provisioning tooling (documented in the EMStool Internal Infrastructure Runbook — internal only, Section 2.7) — this tooling itself is never installed on client hardware; it only creates and configures the two containers described in Section 2 above.

Remote provisioning is not yet supported

As of this writing, EMStool can only provision a deployment onto hardware that is physically in hand — an EMStool-provided appliance is set up before it ships to the client, or a client-owned box is provisioned while temporarily in EMStool's possession. There is no remote installer yet that provisions a deployment on hardware already in the client's own facility; that capability is tracked as a future item (a cross-platform install script, not yet built).