Security
Row-level security in the database, credentials in an encrypted vault, host-only sessions per door, SSH guarded by fail2ban, backups before every migration, and a test suite that signs in as the application role.
In the database
Every tenant's rows are separated by policy, enforced by the database for every query the application makes, and the application's own role cannot bypass it. Secrets — channel keys, gateway keys — are in a vault schema the application role cannot select from. Functions that cross tenants are few, named, and refuse anyone who is not the platform.
At the door
Each door — the PMS, the POS, the CRM, the central desk — is its own host with its own session; signing in on one is not a sign-in on the others. TLS everywhere, one certificate across the doors, renewed automatically.
On the box
SSH guarded by fail2ban with escalating bans; a key, not a password, for deploys; the application listening only on the loopback behind nginx; nothing that executes on the static sites.
Before every change
A database dump before every migration, kept beside its number; a trial run of the migration and its test suite inside a transaction that rolls back, before it is applied for real.
What the suite proves
Nearly two hundred test files, each rolling back, several of which sign in as the application role and try to read another tenant's rows, call the vault, or become a platform user. They fail the build when they succeed.