PadFlow

Security

How PadFlow is built and run, and how you can check it.

Last reviewed September 15, 2026

You put a deal in here: the schedule it closes on, the budget it is underwritten to, the draw file the lender funds against. This page says how that is kept separate, kept encrypted, kept backed up, and who else touches it. It describes what the software does today, not what is planned, and it changes in the same pull request as the behaviour it describes.

One workspace cannot reach another

Every row PadFlow stores carries the workspace it belongs to, and row-level security in Postgres decides what a query is allowed to see. That decision is made by the database, underneath the application, so a request signed in to one workspace cannot read or write another’s rows even if the code above it asks: the refusal happens below the place a bug could be. Every table that carries a workspace has row-level security turned on and a policy attached, and the handful that carry none are the ones no client key may touch at all — under row-level security an absent policy denies everybody, which is a stronger posture than any policy could describe.

Where one record hangs off another — an allocation off a rule, a distribution off a closing — the reference is a composite key that carries the workspace and the project as well as the row. A child cannot point at a parent in someone else’s workspace because the key itself refuses it, rather than because somebody remembered to write a check.

This is proved rather than assumed. A suite seeds two synthetic workspaces, attempts the crossings from each of them, and asserts the exact error each refusal raises — an earlier version asserted only that a forbidden write failed, and one of them was failing on a unique index while reporting itself as isolation working. It runs inside a transaction that rolls back, so it can be run against production, and it is re-run after any change to a policy, to the membership model, or when a table is added.

Inside a workspace, who sees what

Access is assigned per class and per level, and scoped to the projects a person is on. The classes are Schedule & dates, Money, Documents and Settings. The levels run from none through view and edit to approve, and Money carries one more: a level that shows the rows, the cost codes and the status with the amounts withheld, so a superintendent can track progress without seeing price and a lender can see one deal without seeing the next.

A withheld figure is a dash. It never means zero, and no screen sums it into a total. Redaction is applied by the database, through separate views, rather than by the screen that draws the number — so an amount a level does not carry is not in the payload the browser received, waiting to be found in it.

Encryption

Traffic is TLS end to end, and every response carries a strict-transport header so a browser will not try it any other way. The database is managed Postgres, encrypted at rest. The credentials PadFlow holds on your behalf — the refresh tokens behind a Microsoft 365 or QuickBooks connection — are encrypted with AES-256-GCM before they are written, on top of that, and the tables holding them are among those no client key can reach: they are touched server-side and nowhere else.

Backups, and getting the data back

The database is on a plan with daily backups kept for a week, taken and restored by the provider’s own tooling. Behind that is a second route that does not depend on their dashboard being available: the schema rebuilds from the migrations in this repository, and the data is dumped through a documented procedure into a plain file.

Both halves have been run rather than assumed. A restore drill rebuilt the schema into a fresh database with no migration failures and matched production exactly on policy count, tables under row-level security, redaction views and functions; it found two defects in the tooling that no amount of reading would have found, and both were fixed. A test suite holds the dump to round-tripping, including values carrying a newline, a quote, a backslash or a null byte, because a backup that cannot be loaded back is not a backup.

How it runs

The application accepts no inbound connection from the internet. It listens on the loopback interface of one server and reaches the world through an outbound tunnel, so there is no application port to find. TLS, DNS and protection from abuse sit at the edge in front of it.

A deploy that does not come up is rolled back on its own: the deploy builds, restarts, and then asks the health endpoint which commit it is serving — anything but the one just built and the previous release goes back in. Two watchdogs poll from outside, one for liveness and one that renders a real project page through a read-only connection, so a read that fails inside a page is visible to something other than an error report. Errors go to Sentry through a scrubber that strips known secrets before anything leaves the server.

Every page goes out with a content security policy. For each response the server picks a random value that only that page carries, and the policy tells the browser to run a script only if it carries the same value — so a script that was not part of the page as served, whatever put it there, does not run. Framing by another site is refused outright, apart from the Outlook add-in’s own pane, which only Outlook may frame. The script rule is in place today in reporting form: the browser reports what it would have refused rather than refusing it, until those reports have been read, and one setting on the server — not a code change — turns reporting into refusing.

How changes ship

Every change goes through a pull request and a continuous-integration run that has to be green for the exact commit being merged — lint, types and the whole test suite — and a run that was green for an earlier commit does not count. A change that touches the schema gets a second run against a PostgreSQL built from the migrations in this repository, which is where the isolation, policy-coverage and backup suites execute. A migration has to be a no-op when it is deployed: a backfill at least as permissive as what came before, with anything breaking requiring a deliberate administrative step.

An independent security review of the whole repository was carried out in August 2026, against a running system rather than on paper. It found no cross-workspace read or write and no authentication bypass reachable by an ordinary signed-in customer, and it recorded eleven controls that were tested and held — a control proven to work being a result rather than the absence of one. It also raised twelve issues, none of them critical, ranked and tracked in the same repository as the code. A review is a date rather than a state; the suites above are what hold the result between them.

Who else touches your data

PadFlow runs on services that store or move data on our behalf, and only for that purpose. Data is hosted in the United States. The privacy policy is the same list, and says what each one receives and how long anything is kept.

Telling us about a problem

If you think you have found a security problem in PadFlow, write to [email protected] with “Security” in the subject, and say enough that we can reproduce it. We will confirm we have it, tell you what we find, and credit you if you would like to be credited.

Give us a reasonable chance to fix it before you publish. We will not pursue anyone who reports a problem in good faith and who, in finding it, did not read, change or destroy data belonging to somebody else — and if you are not sure whether something counts, write to us and ask before you go further.