# Security

> **Read the last section first** if you are evaluating Calcutab. It says what we
> do not have, which is the part a vendor usually makes you dig for.

This page is written for the person who has to sign off on us. It describes what
is actually built, not what is planned.

## The one that matters: your data cannot reach another customer

Every record carries the workspace it belongs to, and every query is scoped by
it. That is the ordinary answer, and on its own it is only as good as the
developer who last wrote a query.

So the database enforces it as well. Time entries, rates, Jira credentials and
sign-offs reference their parent by a **composite foreign key** of
`(id, workspace)`. Postgres itself rejects a row that points into another
customer's workspace: not a `WHERE` clause somebody can forget, but a
constraint that makes the mistake impossible to store.

Three of the automated checks below exist to prove this, by trying it.

## Access, and money

Four levels: platform operator, workspace owner, manager, member. Every page and
every action checks on the server. Hiding a menu item is not access control, and
we do not treat it as such; the checks run again even when the link was never
shown.

**Money is omitted, not hidden.** A member cannot see any cost, including their
own rate. The figures are not sent to their browser at all, so there is nothing
to find in the page source.

## Credentials and secrets

| What | How it is held |
| --- | --- |
| Jira API tokens, Slack tokens | AES-256-GCM, encrypted before reaching the database, versioned so keys can rotate. Never returned beyond the last four characters, never logged, never written into an audit row |
| Passwords, where a workspace uses them | Salted scrypt, with the cost stored alongside each hash so it can be raised. Never recoverable, by us or anyone |
| Invitation and password-reset links | Only the SHA-256 of the token is stored. A leaked database completes neither |
| The encryption key itself | Google Secret Manager, never in the repository |

Most workspaces have no password at all: sign-in is Google or Atlassian, so
their provider's two-factor and offboarding rules apply and there is nothing for
us to lose.

## In transit, and in the browser

All traffic is TLS. Session cookies are `Secure`, `HttpOnly`, `SameSite=Lax`.

Every page carries a **Content-Security-Policy built per request**, with a fresh
nonce: scripts are allowed by nonce rather than by `unsafe-inline`.
`frame-ancestors` is `none`, so the app cannot be framed.

## What is recorded

An append-only audit trail: rate changes, membership changes, settings, period
sign-offs, and any timesheet edited on somebody else's behalf, with who, what,
from what to what, and when. Exportable as CSV.

Rates are never edited. A change inserts a row with a new start date, so a
report run last year still produces last year's figure.

## Abuse

Writes and sign-in attempts are rate limited. Eight consecutive failed password
attempts shut an account for fifteen minutes: passwords only, because locking
the Google and Atlassian paths would let anyone deny a colleague access by
guessing at their address.

## Where your data is

Google Cloud Run in `europe-west1` (Belgium); PostgreSQL on Neon in AWS
`eu-central-1` (Frankfurt). Backups stay in the EU. The complete sub-processor
list is in the [DPA](/legal/dpa), section 7; it is the whole list, and changing
it changes that document.

## Checked before release

Fourteen automated checks run against a real deployment, not a mock, covering:

- a manager reaching into another workspace's membership, projects, reports and
  CSV export
- a member reaching the rate card, the audit export, or the operator's page
- a session signed with the wrong key
- unauthenticated access to pages that require one
- stack traces, deployment details and policy headers

The script refuses to run at all unless a minted session is first accepted: an
earlier version reported eleven passes against what turned out to be empty
redirect bodies, which is the kind of result that is worse than no result.

Alongside these: 936 unit tests, 233 end-to-end tests against a real browser and
database, and 216 integration tests.

## If something goes wrong

A written incident process covers who is told, in what order, and within what
time. Health and error alerting reaches the operator if the service or its
database becomes unreachable from two regions.

Report a vulnerability to **support@calcutab.com**. We will acknowledge within
one working day. We do not currently offer a bounty.

## What we do not have

Stated here rather than left for you to discover:

- **No independent penetration test.** The checks above are ours. A third-party
  test will be commissioned when a customer needs the report; we will not
  pretend one exists.
- **No SOC 2 or ISO 27001.** Neither is in progress. Both are a year's work and
  a five-figure cost, and we would rather say so than imply otherwise.
- **No on-call rota.** Alerting reaches one person. Outside working hours,
  recovery depends on that person.
- **No contractual uptime or recovery-time commitment.** The terms say this too.
- **No customer-managed encryption keys, no SAML, no SCIM.** OIDC per workspace
  is feasible and unbuilt; SAML needs a broker. Neither exists today.
- **The legal documents have not been reviewed by external counsel.** They are
  published as version 1.0 and describe what the software actually does; their
  legal sufficiency in your jurisdiction is not something we have had tested.

If one of these is a blocker for you, say so; it is better to know before you
buy than after.
