Security and privacy
Tenancy isolation, two-step verification, impersonation, file uploads and the audit trail.
What protects one agency's data from another's, one person's account from a stolen password, and the record of what happened from being quietly edited afterwards. Written to be readable by the person at a client who has been asked to review it.
#Tenancy isolation
One agency is one tenant. Every database query the application makes is scoped by agency — not as a filter applied to results, but as part of how a record is looked up in the first place.
- A cross-tenant id returns 404 Not found, not "access denied". A denial would confirm the record exists.
- Every list, search, export and audit view is scoped the same way. There is no view anywhere in the agency portal that reaches across tenants, and no query string that produces one.
- The unscoped view exists only for platform operators, and which one you get is decided by who is asking rather than by anything in the request.
Within a tenant, clients are separated the same way: an agency user's access to a client's records is scoped through the client, and a client's own users can only ever reach their own client.
#Signing in
- Two-step verification is mandatory
- Every account, no exceptions, no way to switch it off. Email is the only second factor.
- There is no password-reset flow
- Deliberate. Email being the second factor means a reset link sent to the same mailbox would collapse two factors into one. Getting in requires the password and the mailbox.
- Passwords are hashed with bcrypt
- Never stored, never logged, never recoverable — including by us.
- Sign-in attempts are rate-limited
- Five failures within five minutes locks an email address for five minutes. The lock is on the address, not the browser.
- Codes are single-use and time-boxed
- Six digits, ten minutes, five wrong attempts before the code is burned.
- Failures are deliberately vague
- Invalid email or password, whether the account exists or not. An unknown subdomain returns a plain 404 with no sign-in form, so the platform never confirms which agencies exist.
#Who can do what
Access is decided by permissions carried on a role, attached to a membership. Two properties are worth stating:
- Checks are on the server, not in the interface. A hidden button is a courtesy; the action behind it refuses the request independently.
- Reads are gated on the module, writes on permissions. A client-side Viewer can open every enabled section and change nothing.
Full detail: Roles and permissions.
#Read-only demotion
Two situations collapse a session to its read-only permissions rather than ending it: an offboarded client, and a past-due agency. In both cases the person keeps their history and loses their buttons — and the actions behind those buttons are refused, not merely hidden.
#Impersonation and view-as
An agency can open a read-only view of a client's portal; a platform operator can do the same for an agency. Four properties hold in every case, without exception:
| Property | Means |
|---|---|
| Always read-only | Every write is refused at the server, not hidden in the interface. |
| Always bannered | An amber bar with a live countdown, visible to anyone looking at that screen. |
| Always time-boxed | It ends on its own. Stop viewing ends it sooner. |
| Always audited | Recorded with the reason the operator was required to type before starting. |
There is no silent mode, no way to suppress the banner, and no way to write while impersonating.
#The audit trail
Every action that changes something writes an entry naming the actor, the time and the request address. Entries are append-only — nothing in the product can edit or delete one, including a platform operator.
Agencies read their own log at Settings → Audit log, gated on agency.view_audit, and can export it as CSV.
#Contracts get something stronger
A contract carries its own hash-chained event ledger: created, sent, viewed, identity verified, consent accepted, signed, completed, downloaded. Each entry's hash covers the one before it, so an entry cannot be altered or removed without breaking every entry after it — and the chain is verified when the page is drawn, with the result shown alongside it.
The executed document is sealed with a SHA-256 digest, a signature over it, the key identifier and the byte count. See Contracts → the evidence.
#File uploads
Files never pass through the application. The browser asks for a one-time upload URL, sends the file straight to storage, and then confirms — and only that last step writes the database record.
- A failed or abandoned upload leaves no half-created record behind.
- Upload URLs are single-use and short-lived.
- Stored files are encrypted at rest, and reachable only through the application's own routes.
#Transport and headers
- Everything is served over TLS; plain HTTP is redirected.
- Session cookies are HTTP-only, so page scripts cannot read them.
- A web application firewall sits in front of the application.
- Errors are collected centrally so a fault affecting one tenant is visible without anyone reading another tenant's data to find it.
#Destructive actions
Three levels, applied consistently:
- A confirmation dialog for anything that cannot be undone with a click — deleting a time entry, removing a location.
- A confirmation that names the consequences where they are not obvious. Removing a website names the audits, crawls and backlink snapshots that go with it; terminating a team member names their open workload.
- Typing the name to confirm for the worst of them: deleting a client, deleting an agency.
Closing a client account defaults to a 30-day window during which nothing is destroyed and the decision can be reversed, with a reminder seven days before the deadline.
#Getting your data out
- A client's records can be exported while the account exists — do it before scheduling deletion, not after.
- The audit log exports as CSV, honouring whatever filters are applied.
- Executed contracts download as PDFs with their certificate of completion bound in.
- Invoices print, or print to PDF, from their own page.
#What we cannot do
- Read your password. It is hashed.
- Set a new one for you. There is no such path in the product.
- Edit or delete an audit entry. Append-only means append-only.
- Look at your data without leaving a trace. Reaching a tenant's records means an impersonation session — bannered, time-boxed, and audited with a typed reason.