Legal

Security and data handling

What is stored, what is refused, where it lives, who can see it, and what the system is built never to do — each claim pointing at the code or configuration that makes it true.

Draft for approval · effective date to be set when this text is approved

What is stored

Every file you upload, content-addressed in a private bucket (source-artifacts, engine/storage.py), with its name, size, SHA-256 hash, and the property and month it belongs to. An Excel workbook is kept as the text of the one sheet that was read and scanned. From its contents: reservation and confirmation numbers, dates, amounts as integer cents, channel and status, guest names where your export includes them, and — only where your export already includes them — the last four digits of a card and its brand. Every exception the comparison opens, the two records it cites, the deadline computed for it, and every decision taken on it, in an append-only log.

A file whose columns no mapping recognises is kept too, with its first five lines stored beside it, so a mapping can be written from what actually arrived. It is not compared until the mapping exists, and it is deleted on request.

From Clerk: name, email, organisation, role. From Stripe: customer and subscription identifiers. Card details never reach us.

What is refused

A full card number. At upload, before a CSV, Excel or PDF file is hashed, stored or queued, its text — a workbook’s sheet, a PDF’s extracted text, and the file name — is scanned for 13–19 digit runs that pass the Luhn check (src/lib/ingest/pan.ts, src/lib/ingest/pipeline.ts). A hit refuses the whole file with nothing written; the error names the line number and never the digits, because a helpful error message is how a card number ends up in a log. There is deliberately no column a card number could be written to.

The engine scans every file again before parsing it (engine/ingest/pan.py, engine/storage.py). A hit there, or a PDF with no text layer to scan, deletes the object and refuses the file. If the PDF text reader fails to load at upload, the PDF goes to quarantine without the first scan and the receipt says so; the engine’s scan is the one that always applies.

Text is decoded before it is scanned (src/lib/ingest/text-decode.ts): a UTF-16 “Unicode Text” file is read by its byte-order mark, because read as UTF-8 a card number becomes a digit, a NUL, a digit, and no scan sees it. A file with NUL bytes and no such mark — unmarked UTF-16, or a binary file renamed to .csv — is refused with nothing stored. Only CSV, Excel and PDF files are accepted.

Where it lives

Postgres and file storage on Supabase, AWS us-west-1. The web application on Vercel. The reconciliation engine on DigitalOcean App Platform in the San Francisco region, as a worker with no public hostname — there is no endpoint to send a request to, which is the security posture rather than an omission. Sign-in by Clerk; payments by Stripe; email by Loops; support email in Freshdesk.

Two prefixes in the same private bucket hold bytes no scan has passed yet, and neither is readable by any organisation (the bucket’s read policy keys on the organisation folder). staging/: a larger upload goes from the browser straight here and waits only until the server has scanned it; the staged copy is removed whether or not it passes, and one that is never finished is deleted after two hours. quarantine/: a PDF waits here until the engine’s scan, then moves to its permanent key or is deleted; one whose month is never run is deleted after seven days. The sweep runs inside the engine’s worker loop and lists and deletes keys without ever reading a file.

Operational alerts go through Loops to the CactusStay administrators. An alert about an unrecognised file carries only the line identified as its column row, with every digit masked, never the rest of the file; an alert about a refused card number carries line numbers only; a server error alert carries the route, never the request or the error message (src/lib/ingest/ops-alert.ts, src/instrumentation.ts).

Who can see it

Members of your organisation, by role: owners manage properties and billing, bookkeepers upload and work the queue. Every read and write in the application is scoped to your organisation. The database has row-level security switched on; the policies that would make it a second, independent check are written (engine/migrations/0006_rls_policies.sql) but not yet switched on, so today the application’s scoping is the one that holds. We say so rather than claim two layers where there is one. Osonwanne Group operators can access your data to run the service and to support you; nobody else can, and no other customer ever can.

How long

Records and results are kept for as long as the organisation’s account exists, because an evidence pack must be reproducible from the original inputs years after the month it covers. Each file is stored once, under a key derived from its hash, and a run’s job carries artifact ids rather than file contents, so a re-run reads the same bytes the first run read. A fixed deletion period after an account closes has not been set yet; until it is, deletion is on request and we say so rather than quote a number.

What the system never does on its own

It never submits a dispute to an OTA, never posts to a property management system, and never charges a card. Amounts, matches and deadlines are computed by deterministic code; a model may read a document and propose a label from a closed list, and may never compute an amount or act. An unrecognised settlement instruction stops a reservation rather than guessing at it, and a variance nothing explains is reported as exactly that.

What is still unknown

Whether handling virtual-card references — the last four digits and an opaque reference, never the number — puts the service inside PCI-DSS scope. Until that is settled the design assumes it might, which is why the boundary refuses a full number rather than promising to protect one.

Reporting a problem

If you believe a file with a full card number was accepted, or you can see data that is not your organisation’s, tell us immediately at support@osonwanne.freshdesk.com. The five questions page lists exactly which exports to pull and what to leave out: never a full card number; guest names stay in where your agreements allow, because they are how Booking.com stays are matched.