Retention
What we hold, for how long, and what deletes it — 26 tables · 15 delete jobs · 10 no job touches. Every window in this table is read from the same module our delete jobs run on, so the policy and the deletion cannot drift apart.
Two clocks
Where a row has a window, it is a maximum of 7 days — operational and personal-linked rows alike — and that maximum is not a target: a row is deleted when its window closes, and where it can be let go sooner it is. 10 tables carry no window at all; the table below says which, and why.
Invoices, subscription and payment records are kept for 5 years because the ATO requires it — not because we want them. They are not logs. The 5 years is an obligation with a period on it, and it is the reason those rows carry no delete job in the table above: deleting them sooner would be failing the obligation, not tidying up.
Every table, and what removes it
One row per table in our database, including the ones we never delete. Where the enforced-by column names a job, that function is the only scheduled deletion for the row.
| table | what it holds | personal? | class | retention | enforced by |
|---|---|---|---|---|---|
account | The account itself: the name it is billed under, and when it was opened. | yes — who pays, and what they pay for | financial | kept for 5 years for the ATO — no job deletes it | no job — the ATO's period is an obligation, not a task |
user | The person behind a sign-in: their email address, when it was verified, and whether they are a customer or an operator. | yes — who pays, and what they pay for | financial | kept for 5 years for the ATO — no job deletes it | no job — the ATO's period is an obligation, not a task |
magic_link | A one-use sign-in link: a hash of the token, when it expires, and whether it was used. | yes — a person, their browser or their address | personal | deleted after 7 days | pruneMagicLinks |
session | A signed-in browser: a hash of the cookie, its expiry, and the user-agent string the browser sent. | yes — a person, their browser or their address | personal | deleted after 7 days | pruneSessions |
plan | Our price list: the plan's code, name, price, billing interval and device limit. | no — our own price list | catalogue | not deleted — no job removes it | no job — nothing deletes it |
subscription | A subscription: which plan it is for, its status, the processor's identifiers, and the end of the current period. | yes — who pays, and what they pay for | financial | kept for 5 years for the ATO — no job deletes it | no job — the ATO's period is an obligation, not a task |
entitlement | What a subscription unlocks: the feature, its limit, and when it was granted or revoked. | yes — who pays, and what they pay for | financial | kept for 5 years for the ATO — no job deletes it | no job — the ATO's period is an obligation, not a task |
node | One of our machines: its name, region, hostname, public key, endpoint, tunnel pool, agent version and last heartbeat. | no — our own machines and workers | infrastructure | not deleted — no job removes it | no job — nothing deletes it |
pop_deployment | Our ledger of the machines we have rented: the name, provider instance id and public address a PoP claimed. Never deleted, because a spent identity must not be handed back to the next machine. | no — a routing change, a peer key and an instant | lifecycle | not deleted — no job removes it | no job — nothing deletes it |
node_token | The credential our agent signs in with — a hash of the token only, and when it was revoked. | yes — a credential or a device's identity | identity | deleted after 7 days | pruneNodeTokens |
node_enrolment | A one-use token that enrols a new machine, and whether it was consumed. | yes — a credential or a device's identity | identity | deleted after 7 days | pruneNodeEnrolments |
device | One of your devices: the name you gave it, its public key, the tunnel address it was given, the counters the node last reported, and its handshake times. | yes — a credential or a device's identity | identity | deleted after 7 days | pruneDevices |
device_config_link | A one-use link that delivers a device's configuration: a hash of the token, when it expires, and whether the key was fetched. | yes — a credential or a device's identity | identity | not deleted — no job removes it | no job — nothing deletes it |
node_event | A transition on a machine — a peer added, removed, moved or reset — with the peer key and the instant it happened. | no — a routing change, a peer key and an instant | lifecycle | deleted after 7 days | pruneNodeEvents |
node_health | One poll of one machine: whether it was reachable, the latency we measured, its agent version, uptime and peer count. | no — a measurement, not a person | sample | deleted after 7 days | pruneNodeHealth |
node_peer | The peers a machine reported in its last dump: public key, your address and port, the addresses allowed through it, the last handshake and the byte counters. Replaced on every report. | yes — a person, their browser or their address | personal | deleted after 7 days | pruneNodePeers |
peer_drift | A disagreement between the peers we expect on a machine and the ones it reports, open until it stops being true. | yes — a person, their browser or their address | personal | deleted 7 days after the row resolves — an open row is never deleted, whatever its age | prunePeerDrift |
worker_run | One run of one of our workers: when it started, when it finished, whether it succeeded. Fleet-level — the row names no account and no machine. | no — our own machines and workers | infrastructure | deleted after 7 days | pruneWorkerRuns |
worker_lease | Which worker is running right now, and until when. | no — our own machines and workers | infrastructure | deleted after 7 days | pruneWorkerLeases |
node_traffic | The sum of a machine's peer counters at one instant and how many peers that covers — a total, with no device identity on the row. | no — a measurement, not a person | sample | deleted after 7 days | pruneNodeTraffic |
client_session | A signed-in desktop client: a hash of its token, the name you gave the computer, and when it was last seen. | yes — a person, their browser or their address | personal | deleted after 7 days | pruneClientAuth |
client_device_code | A code a desktop client shows you, which you approve in a browser: hashes, the short code, and when it was spent. | yes — a person, their browser or their address | personal | deleted after 7 days | pruneClientAuth |
device_move | A device moved between machines: the machine, key and address it left, and the ones it arrived on. | no — a routing change, a peer key and an instant | lifecycle | deleted after 7 days | pruneDeviceMoves |
region | The catalogue of places we sell: a code, the place's name, its country and a sort order. | no — our own price list | catalogue | not deleted — no job removes it | no job — nothing deletes it |
admin_view | A record of an operator opening the cross-tenant view: which address looked, at which page, when, and whether the gate allowed it. Nothing you typed. | yes — a person, their browser or their address | personal | deleted after 7 days | pruneAdminViews |
schema_migration | Our own ledger of which schema file was applied, and when. | no — our own machines and workers | infrastructure | not deleted — no job removes it | no job — nothing deletes it |
The sharpest row we hold
The one thing in this database that is not anonymous, and the clock it gets instead of the seven-day one.
One row is sharper than the rest and gets its own clock. node_peer.endpoint is your real address and port as the machine last saw it — the least anonymous thing we hold. It is erased 24 hours after the address was last new information — when it changed, or when a handshake advanced — so a machine that keeps reporting the same address does not push that clock forward. The 7-day ceiling in the table above is a ceiling, not a target: an address does not wait for it. peer_drift.detail keeps a copy of that address, and that copy is erased 24 hours after the disagreement is detected or resolved, whichever is later — an open disagreement loses the address and keeps the row. The 7-day deletion for that row is a different clock: it counts from the moment the row resolves — 7 days after it resolves, not from the day it was detected — and an open row is never deleted, whatever its age.
What we never collect
Each of these is a statement about the schema, not a promise about intent — there is no column for any of it.
- No traffic content, no packet capture and no deep packet inspection: we do not copy, store or read the packets that cross a tunnel.
- No DNS query log: nothing we hold records the name you looked up.
- No destination IP history: no row records where you connected. The only public address on a peer row is your own, and the paragraph above says how long we keep it.
- No per-device traffic attribution: the throughput series is stored summed — node_traffic carries a total and a peer count, with no device identity on the row — which is why it needed none of the per-account scoping our peer table has (decisions/0034 D4). What a device row carries is the counter the machine last reported for it: a current value, not a window.
- No keystroke, application or process monitoring: no column anywhere records what you typed, which program you ran, or what ran beside it.
One limitation, stated rather than omitted
Who can see across accounts, what the gate records when they do, and what it cannot tell you.
One limitation is ours to state, and we would rather state it than not. Four addresses hold the cross-tenant view — every account, every device, and which machine it sits on: [email protected], [email protected], [email protected] and [email protected]. They hold it because user.role is either 'customer' or 'admin': there is no read-only role between the two, and that limitation is real and it stays. What we can tell you is who opened it. Each time a signed-in session reaches the cross-tenant view, the gate writes the row to admin_view before it answers: the operator's address, the page that was opened, the instant, and whether the gate allowed it or refused it — a refusal is recorded too, because a role taken away mid-session is a fact worth keeping — and a write that fails is counted and reported to our own health checks rather than allowed to fail the page. The row is deleted after 7 days, counted from the view itself, on the same clock as the rest of the table above, so the answer reaches back exactly as far as this policy keeps anything. What it does not carry is the content: no query string, no body, no copy of your records, nothing that says which account or device was on the screen, and no address for the machine that asked, because we will not write a request header into a log as though it were one. So we can tell you which of the four opened the view, and when, for those 7 days; we cannot tell you what they read while it was open.