Security

The controls, and the things we deliberately cannot do.

Written for somebody who intends to check. Every control below corresponds to code in this repository, and everything we have not built is listed as plainly as everything we have.

Isolation policies
91Forced, on every tenant table
Capabilities
67Across 8 roles
Integrity rules
25Over the whole ledger
Government connections
0By design, and by absence of credentials

Isolation the database enforces

91 row-level security policies, declared with FORCE so they apply to the table owner too, and a restricted application role created NOSUPERUSER NOBYPASSRLS. Posting sets the tenant per transaction with set_config; reporting and other reads are scoped by the query, and extending set_config to them is in progress.

It refuses to start in an unsafe posture

PostgreSQL silently ignores every policy for a superuser. A deployment connected as postgres therefore has all its isolation policies doing nothing while every screen looks perfectly healthy. The application inspects its own connection at startup and refuses to serve production traffic in that state — because that is a failure nobody would otherwise notice.

A tamper-evident audit trail

Every recorded event is hashed together with the hash of the event before it, per tenant. Editing a row directly in the database breaks every subsequent hash, and the Operations screen reports exactly where the chain stops verifying. Verified across 12,050 events in the load test.

Capability-based permissions

67 named capabilities across 8 roles, checked at the server action rather than in the component that renders the button. A hidden button is a design choice; an unauthorised action is a rejection.

API keys that cannot exceed their holder

Public API keys carry a subset of capabilities and can be pinned to a single company. A key can never hold a capability its creator lacks, so an API key is not a route around the permission model.

Uploads treated as hostile

Every uploaded file is checked by content rather than by extension, size-capped, and stored outside the web root with a generated name. A statement import is the one place a stranger’s file meets your books.

Never stored

The list, in full

Not a philosophy. Seven specific things that are absent from the schema, and can be confirmed to be absent from it.

  • Plaintext passwords — credentials are hashed with a slow, salted algorithm and never logged.
  • Bank credentials. There is no bank feed, so there is nothing to store; statements are files you import.
  • GST portal credentials. Nothing here can log in to a government system.
  • API keys in the repository. `.env.example` carries names, never values.
  • Payroll figures in logs. Salary data never reaches a log line, at any level.
  • Card or payment details. There is no payment provider connected.
  • Anything from a government portal. No portal is scraped, ever.

The chain is per tenant and sequenced, so two companies writing at the same moment cannot interleave into each other’s history. Verification walks it end to end and reports the first sequence number where the recomputed hash stops matching.

Statutory work

Nothing is filed on your behalf. Ever.

This is the single most important sentence on the site, so it gets a section rather than a footnote.

  • No credential exists. There is no field, column or environment variable anywhere in this system for a GST portal login, a digital signature, or an e-invoicing provider’s production key.
  • The integration is a mock. The e-invoicing and e-way bill screens run against a provider interface with a mock implementation. It returns SANDBOX identifiers and the screen says so before it shows you one.
  • Those identifiers must not reach a customer. A SANDBOX IRN issued as a real e-invoice is a compliance failure. The screen states this in as many words, and it is repeated in the footer of every page on this site.
  • If a real provider is ever connected, it will be behind an explicit authorised action, a final validation, a confirmation screen, logged consent, stored provider responses, and idempotency protection — the design is already written that way, and none of those steps exist to be skipped.

Questions

Asked by the people who ask hardest

Are you SOC 2 or ISO 27001 certified?
No. Neither audit has been carried out, and claiming otherwise would be straightforwardly false. What is on this page is what has actually been built and can be checked in the code — which is a different kind of assurance, not a substitute for one.
What happens if someone gets read access to the database?
They read your data — that is what read access means, and no application-level control changes it. What they cannot do is change it quietly: every write is an audited event in a hash chain, so a modified row breaks every hash after it and the verification screen names the point it broke.
Where does my data live?
Wherever you put it. It runs on any PostgreSQL 15 or later, self-hosted or managed. Development and the test suite use an embedded PostgreSQL that writes to a local file — which is also why the application warns loudly when that embedded database is used in an environment pretending to be production.
Does any of my data go to an AI provider?
Only when you use a drafting feature, and only the text you gave it. Ledger balances, payroll and bank statements are never sent anywhere. If no provider key is configured, the drafting features fall back to a local parser and nothing leaves the machine at all.
How do I report a vulnerability?
Write to hello@godengine.io with enough detail to reproduce it. You will get a human reply, and we will not argue about whether it counts.

Read the code, then decide.

Everything on this page is a property of the system rather than a policy about it — which means it can be checked.

Security · Falcon