Security, governance and compliance
The safeguarding rules are enforced by the database, not written in a policy nobody can check
A written policy is enforced by the person it constrains. A database policy is enforced against everyone, including us, including a bug, including a query somebody wrote in a hurry. This page is an account of which of our safeguarding rules are held that way — and, in equal detail, which parts of a security posture we have not built yet.
- Where do the rules live?
- In PostgreSQL, as row-level security on all 35 tables. Not in application code that a future feature can forget to call.
- Who proves it?
- 266 assertions run against a live database on every change, each stating something that must not be possible.
- Are you certified?
- No. This is a pilot-stage product. It has never been penetration-tested and has no DPIA. Both are listed below with an owner.
A note on how to read this. Every claim below is a claim about code that exists today, and the file it lives in is named so your own technical people can ask to see it. Where something is a decision, a roadmap item or a risk, it is written as one. This product exists so adults can message children, and a page that overstated its position would be the same failure it is meant to prevent.
Tenant isolation
One organization cannot read another’s messages, and the application itself has no way to arrange otherwise
Multi-tenant products usually enforce this in code: every query carries a filter, and the guarantee holds for exactly as long as every developer remembers. Here the filter is a property of the table, so a query that forgets it returns nothing rather than everything.
Row-level security on all 35 tables
Every table in the database carries a policy naming which organization may see which rows. A query that forgets to filter by organization returns nothing rather than everything.
Written by hand in one readable file rather than generated, so a reviewer can check the isolation rules without trusting a code generator.
And FORCE, which is the part that is usually missed
Switching row-level security on still exempts the table’s owner — and the owner is the role that runs migrations. Without FORCE, isolation reads correctly in review and enforces nothing.
Every table is FORCEd, and the migration refuses to finish if one of them is not. It reports each table by name as it goes.
The application does not own its tables
The role the running application connects as is not the owner of the data and is not a superuser, so it cannot step around a single policy. Migrations run as a different role entirely.
The database client refuses to start against an owner or superuser connection string. Connecting as the owner would silently disable every policy above and nothing would look broken.
- What it is
- Row-level security on all 35 tables, FORCEd so even the table owner is subject to it, with the application connecting as a role that owns nothing.
- Why that is unusual
- The usual approach puts the boundary in application code, where one forgotten WHERE clause is a cross-tenant leak. Here the mistake produces an empty result rather than another organization’s data — and the migration will not finish if a single table is left unprotected.
- What it saves you
- Your board’s question — “could another organization, or one of your own staff, ever see our young people’s conversations?” — is answered by the database schema rather than by a promise about developer discipline.
How it is proven
Every claim above is asserted against a real database on every change
A security control that is not tested is a security control that used to work. These are not unit tests over mocked objects: they open a connection, take on the identity of a specific person in a specific chapter, and try to do the thing that must be impossible.
assertions across 12 files, run against a live PostgreSQL database rather than a mock. Each one states something that must not be possible, and a single failure fails the run.
messaging-access.sql
56 checksWho can read a conversation
- one organisation cannot read another
- a leader in a sibling chapter sees nothing
- a suspended leader loses access immediately
- platform staff cannot read a single message
- a sent message cannot be edited
- a message can be hidden, and its body survives
image-access.sql
42 checksPictures, and the fact none are permitted yet
- no jurisdiction currently permits pictures at all
- a reviewer outside the conversation is refused
enrolment-access.sql
33 checksJoining codes and who may issue them
- platform staff cannot read a tenant's enrolment codes
- a code cannot admit anyone but a young person or a guardian
tenant-self-service.sql
28 checksWhat an organization may change about itself
- a tenant CAN raise its consent age above the jurisdiction floor
- a consent age below the jurisdiction floor is refused
- the floor is reachable; the boundary is the floor, not the last value
- guardian view is bounded in BOTH directions
- the seven settings still the platform's are refused, loudly
calendar-scopes.sql
23 checksHolidays, closures and curfew days
- tenant A sees its own date + public holidays, not tenant B
- tenant cannot create a public holiday
- tenant cannot promote its own date across the seam
- a rule that says nothing about holidays ignores them
consent-withdrawal-requests.sql
15 checksA parent's request to withdraw consent
- another family's guardian sees nothing
- a request cannot be deleted by anyone
- a leader reads the queue and cannot decide
onboarding-and-consent.sql
14 checksInvitations, consent and reset tokens
- foreign invitation tokens do not resolve
- reset tokens unreadable and unmodifiable by the runtime role
- reset tokens redeem exactly once, unknown tokens fail
- login identity lookup cannot cross tenants
- consent cannot be forged across tenants
push-subscriptions.sql
14 checksDevices registered for notifications
- a device registration belongs to one person and nobody else
grid-views.sql
11 checksSaved views and private working state
- a colleague sees the team view and not the private one
- another tenant sees no saved views at all
- the platform tier cannot read a tenant
- an unnamed platform context is not a wildcard
tenant-isolation.sql
11 checksThe boundary itself
- no context sees nothing
- tenant context with no org user behind it edits no safety flag
- isolation is symmetric
- audit trail is append-only
- tenant cannot forge platform audit events
message-body-expiry.sql
11 checksRemoved messages and their retention
- an untriaged concern holds a body back from expiry
- the retention floor is re-derived in SQL, not trusted from the caller
worker-runs.sql
8 checksThe record of scheduled work
- a finished run cannot be rewritten
- the api role reads the run log and can write none of it
A sample of the assertions, quoted from the files exactly as they are written there — in English, because they are quotations of the source and you can ask us to run the named file and watch that sentence appear. Two of them exist because a probe caught a real defect in code that had already been reviewed and looked correct: an authentication path that returned no rows, which would have meant login failing for every user, always, and an invitation lookup that only resolved for someone already inside the organization. Neither was visible in review. Both were found in minutes.
The record
Every change leaves a trail, in the same transaction as the change itself
Not a log written afterwards by a well-behaved caller. The record and the trail either both happen or neither does, because an action that succeeded without a trail is precisely the hidden activity this product exists to prevent.
message.sent
Leader · Chapter 14
9f2c41ab7de0…
- seals 9f2c41ab7de0
message.hidden
Supervisor · Region East
c07b5e19a4f2…
- seals c07b5e19a4f2
curfew_rule.updated
Owner · Organization
4ad8f60c3b71…
Messages cannot be edited after sending
The database refuses it, not the interface. There is no UPDATE policy, the privilege is revoked from the running application, and a trigger stops even the owning role. Three layers, because a guarantee made to a safeguarding investigation is worth what its weakest lock is worth.
A deleted message disappears and is kept
Deleting hides the message from the people in the conversation; the original text, the sender and both timestamps remain readable to leaders. The one permitted change to a message row is granted at column level, so no code path exists that could rewrite the words themselves.
And the interface says which it did
A screen that says “deleted” when it means “hidden from you and retained for review” is lying to the person reading it. The thread shows that something was removed, so a gap is never silent.
- What it is
- An append-only, hash-chained audit trail that the running application can add to and cannot alter, written inside the transaction that made the change.
- Why that is unusual
- Most systems can tell you what they currently believe. Far fewer can show that the history has not been edited since — and fewer still refuse the edit at the privilege layer rather than declining to offer a button for it.
- What it saves you
- When a parent, a board or an investigator asks what was said and by whom, the answer is a record that can be handed over, with the property that removing anything from it is detectable.
The line between you and us
An organization configures its own oversight, and cannot reduce it
You set your curfew hours, your consent age, who may message whom. Underneath that sits a floor, set by AngelLink in a console your organization has no access to. The delegation is only safe because of what it cannot reach.
Enforced by policy
Fourteen of the twenty-four safety settings belong to the organization's owner and ten belong to AngelLink. A supervisor can write none of them. The owner writes their fourteen inside the floor for their jurisdiction — refused by name, with the floor quoted back, if they try to go under it — and cannot reach the other ten at all.
Enforced again by trigger
A column grant alone could not do this, and that is worth stating because it caused a real regression: both tiers arrive on the same database connection, so the trigger — not the grant — is what separates them.
And absent from the schema
The update contract has no field for the escalation floor at all, so a service cannot send one by accident. Three independent refusals for one line that must not move.
Why one setting has no “off”
The adult a young person most often needs to report is the leader of their own chapter — the person they see every week. A configuration able to narrow their options to “the leaders of your own chapter” would leave that child able to raise a concern only with the person the concern is about. So the escalation floor is a choice between two safe answers, never between safe and none, and it is checked over the whole configuration cross-product rather than in the cases somebody thought of.
Deliberate limits
Two things this product does not do, and will not
Both look like security features from the outside. Both would make this product worse at the one job it exists for.
This is not end-to-end encrypted, and we will never say it is
End-to-end encryption would mean nobody but the two people in the conversation could read it — including your safeguarding lead. Oversight is the product. The stated objection to WhatsApp is that it has no oversight layer, and building a second WhatsApp with a logo on it would answer nothing.
Messages are readable by the service, and by the leaders the access rules name. Writing that plainly is not a concession: “end-to-end encrypted” is a security claim made to parents about their children’s data, and it is one this architecture cannot honestly make. It is recorded in the build decisions as a permanent prohibition on the wording, not as a feature deferred to a later release.
Working here is not a way to read a child’s messages
The platform tier manages the organization registry and the safety floors above it. On the message tables it is granted nothing, and the probe that says so is worded exactly that way: platform staff cannot read a single message.
This is a deliberate limit on our own power, written down so that any future change to it is a visible decision rather than a drift. It is also the honest boundary of what a support conversation with us can involve: we can act on the configuration around a conversation, and we cannot read what is in it.
What we have not done yet
A security page that lists only what exists is telling you half of a due-diligence answer
You would find all of this anyway — in a questionnaire, in an insurer’s form, or in the week before a contract is signed. Finding it here first is better for both of us, and each line carries the person who moves it rather than an excuse.
Penetration test
Never performed.
Who moves itAn external firm, once the pilot has a deployed environment to test.
DPIA — Data Protection Impact Assessment
Not started, and very likely mandatory: this is large-scale processing of children’s data.
Who moves itYour data-protection officer or counsel, with us supplying the technical description.
External security audit or certification
None. There is no SOC 2 report, no ISO 27001 certificate, and nothing pending.
Who moves itA decision for the pilot, not a claim we are making.
Encryption at rest, and TLS
Not configured. The build runs on a local development database over plain HTTP.
Who moves itUs, as a production requirement, before a first real young person exists.
Secrets management, backups, restore drill
Environment files only. No vault, no rotation, and no tested restore.
Who moves itUs, in the same production hardening pass.
Legal review of the jurisdiction floors
Never performed. Every seeded policy row carries an empty “legally reviewed” date deliberately, so an unreviewed floor is visible rather than assumed.
Who moves itCounsel in each country you operate in.
Data residency across borders
Unresolved. One shared database means an EU or UK minor’s data could sit outside its origin region. Three options are written down; none is chosen.
Who moves itA joint decision, needed before any non-Canadian chapter is onboarded.
Erasure versus evidence
A genuine conflict, not an oversight. The right to erasure and an append-only safeguarding record pull against each other, and the build has already made the technical choice: it retains.
Who moves itCounsel, to write down the lawful basis. Reversing it later would mean rebuilding the audit design.
Incident response, intrusion detection, sub-processor register
None of the three exist.
Who moves itUs for the tooling, jointly for the process — an incident plan nobody in your organization has read is not a plan.
Screen-reader audit
Never performed. Semantics are verified — accessible names on every control, table captions and scoped headers, focus trapped and restored — but nobody has listened to it.
Who moves itUs, next.
Where this list comes from
An internal working document that is re-measured rather than carried forward, because a number nobody re-measures is a claim rather than a measurement. We will hand it to your technical evaluators in full, gaps and all, at whatever point in the conversation is useful.
Which law applies
More than one regime, because your chapters are in more than one country
This is a map so that somebody qualified can rule on it. It is not legal advice, and nothing on this page should be relied on as any.
| Regime | What triggers it | What it requires of the build |
|---|---|---|
| PIPEDA | Canada, the primary operating country | Consent, purpose limitation, access and correction rights, and breach reporting. |
| Quebec Law 25 | Any chapter in Quebec | Stricter consent and transparency than the federal floor. |
| UK and EU GDPR | Wherever the organization operates internationally | Lawful basis, data minimisation, subject access, and cross-border transfer rules. The age of digital consent varies from 13 to 16 by member state. |
| COPPA | Any US chapter | Verifiable parental consent for anyone under 13. |
| Local safeguarding law | Every country | Mandatory reporting duties differ, and the concern-alert path must not conflict with yours. |
Which is why the consent age is not a constant
The EU alone ranges from 13 to 16, and under-13 in the United States brings a different regime entirely. A single hard-coded age would be wrong somewhere, so the threshold is set per organization and read at the moment the consent gate is evaluated. Beneath it sits a per-jurisdiction floor, edited by the platform owner in a console rather than by a developer in a migration — the person who knows the law should be able to correct it without waiting for a release.
Every one of those seeded floors currently carries an empty “legally reviewed” date, deliberately, so that an unreviewed number is visible on the screen rather than assumed to be right.
And the one conflict we cannot engineer away
The right to erasure and an append-only safeguarding record genuinely pull against each other. Deleted messages and audit entries are retained on purpose; a young person’s account cannot be scrubbed in a way that removes evidence of what an adult said to them.
The usual resolution is that safeguarding records are retained under a legal obligation rather than under consent, and erasure is refused for that subset. That reasoning needs writing down and approving by somebody qualified before a first real user exists — the build has already made the technical choice, and reversing it later would mean rebuilding the audit design.
The standing rules
Nine rules the code is held to, quoted rather than summarised
These sit at the top of the repository, are read at the start of every working session, and are enforced in code review. They are reproduced here unchanged — the only reason they are worth showing you is that showing you changes nothing about them.
Fail closed. Ambiguity denies. Unknown age blocks participation.
A member with no recorded date of birth cannot take part. Guessing that somebody is an adult means an unconsented minor in a live conversation.
Never weaken a policy to make a feature work.
If the database refuses something a feature needs, that is a design conversation, not an obstacle to route around. Saying so is the required response.
The runtime role never owns its tables.
The application connects as a role that cannot bypass a single access policy, and refuses to start otherwise.
Every mutation is audited in the same transaction as the change.
The record cannot succeed while the trail fails, or the reverse. An action that happened without a trail is exactly the hidden activity this product exists to prevent.
Never store a raw token or password. Hash only.
Invitation tokens, reset tokens and session tokens exist in readable form once, in transit. What is stored cannot be replayed.
Never reveal whether an account exists — not in the message, not in the timing.
A sign-in page that answers faster for a real address is a directory of your members for anyone patient enough to read it.
No secrets in code, in logs, or in the audit trail.
The audit writer redacts before it writes. That is a safety net rather than the defence.
Safety configuration is platform-tier only.
An organization must never be able to reduce its own oversight. It sets its own rules inside a floor it cannot reach.
All timestamps UTC.
Stored in one zone and displayed in the reader’s, always carrying the zone. A curfew argued in two time zones is a curfew nobody can audit.
Next
Bring your hardest question
Access is by request. We would rather sit with your safeguarding lead, your data-protection officer and whoever your insurer sends than mail a brochure — including the parts of this page that say “not yet”.