Security
How the platform is built
This page is about the software rather than the data. Trust covers where our numbers come from and what we refuse to claim about them. Here is what the platform itself does to protect an account, and what it deliberately does not do.
- Row-level security policies enforced in the database
- 313
- Passwords stored, because none are ever created
- 0
- Endpoints validating input against a typed schema
- 100%
- Privileged credentials shipped to the browser
- 0
There are no passwords to steal
OpenDecision has no password field, because it has no passwords. You sign in with Google or with a one-time code sent to your email, and the platform never stores a credential that could be reused somewhere else.
That removes an entire class of incident at the root: no password database to breach, no reuse across sites, no reset flow to social-engineer. Sign-in requests are additionally screened by Cloudflare Turnstile, so automated attempts against the code flow are stopped before they reach the account.
The browser is never trusted with data access
The application in your browser cannot query the database. It has no privileged key, and none is shipped in the JavaScript bundle. Every read and write goes through the API, which makes the access decision on the server using your identity.
This is enforced architecturally rather than by convention: the browser genuinely does not hold the means to reach the data directly, so a bug in front-end code cannot become a data-access bug.
The database enforces its own rules
Access control is not left solely to application logic. Tables carry row-level security in the database itself, so a query that should not see a row does not see it even if the code asking were wrong.
Sensitive columns go further. The columns that govern account tier are locked at the grant layer, revoking write access wholesale and granting it back only on the specific fields that are safe to change. That fix exists because the obvious version of it silently does nothing: revoking one column against a broader table-level grant is a no-op in PostgreSQL, and the permission looks applied while every column stays writable.
Every endpoint has a budget
Requests are rate limited per endpoint and per caller, with tighter ceilings on the operations that cost money or touch AI generation than on cheap reads. In production the counters are held in shared storage rather than in one server’s memory, so the limit holds across every instance instead of being divided by however many are running.
Request bodies are capped, individual fields are capped independently of the body, and repeated writes carry idempotency keys so a retried request cannot silently become two.
Defence in depth, not a single gate
Authorisation is decided in more than one layer, so no single mistake is sufficient to expose data. The API decides access from your verified identity on every request; the database independently enforces 313 row-level security policies that apply regardless of which code path asked; and the most sensitive columns are additionally restricted at the grant layer.
The practical consequence is the one that matters in an assessment: a bug in application logic does not automatically become a data breach, because the layer beneath it is still enforcing its own rules on the same query.
Nothing reaches the database unvalidated
Every endpoint declares a typed schema for its body, its URL parameters and its query string, and validation runs before handler code executes rather than inside it. A malformed or unexpected field is rejected at the boundary, so handlers only ever operate on data that has already been proven to have the right shape.
Database access goes through parameterised queries throughout. Values are never concatenated into SQL, which removes injection as a class rather than defending against it case by case. Responses are rendered through React, which escapes by default, and security headers are set on every response at the edge of the stack.
Changes are gated before they ship
Security is a property of the delivery pipeline as much as the running system. Commits are blocked locally by a pre-commit hook that runs linting and a full type-check, so code that does not compile cannot enter history. Continuous integration re-runs the same gates on every pull request, alongside the test suite.
Tests run against a real database rather than mocks, which is a deliberate and occasionally inconvenient choice: a mocked authorisation test passes whether or not the policy underneath it exists. Dependencies carrying known vulnerabilities are pinned forward rather than left to float.
Sensitive operations leave a record
Administrative and vendor-facing actions write to append-only audit tables, so what happened, when, and under whose identity is reconstructable after the fact rather than inferred from application logs.
Write endpoints accept idempotency keys, so a retried request cannot silently become a duplicate action. Errors are captured with request correlation identifiers, which means an incident can be traced end to end without attaching a debugger to production.
Secrets never live in the codebase
Credentials are supplied at runtime from managed secret storage and injected as environment configuration. Nothing privileged is committed to the repository, and nothing privileged is compiled into anything a browser downloads: the service-role key exists only server-side, and the front-end bundle has no path to it.
Destructive actions are made hard on purpose
Permanently deleting a product is the most damaging action an administrator can take, so it is the most obstructed. It requires typing the exact product name and a separate passphrase that is never stored in readable form, it locks out after five failed attempts, and it is refused outright unless deletion has been explicitly provisioned for the environment.
It also writes an append-only audit record, and it reads the result back from the database afterwards rather than assuming the statement did what it said.
AI output is quarantined by default
Anything the AI produces enters as local evidence, scoped to the space it came from, and is never automatically promoted into the shared index that informs other people’s results. Promotion requires human review.
The reason is specific rather than general: without that boundary, content a model generated (or that a user uploaded) could steer what other customers are shown. Files you upload stay tied to your own space for the same reason.
What we do not claim
- No compliance certifications. We do not hold SOC 2, ISO 27001 or any equivalent audit, and you will not find a badge for one here. When that changes we will name the auditor and the report date.
- No penetration-test claim. We are not asserting an independent security assessment we have not commissioned.
- No uptime guarantee. We do not publish an SLA, so we do not imply one.
- No claim that any of this is sufficient. These are the controls the platform has today, described plainly so you can judge them against your own requirements rather than take a reassurance on faith.
Reporting a vulnerability
If you find a security issue, tell us before telling anyone else and we will work the fix with you. Reports go to our contact page, and a report about security will be treated as urgent rather than queued behind product feedback.