rilo
The Rilo mascot fox standing beside a shield with a padlock on it

Let’s get technical.

Everything the front page says, written out as the thing it was built from: the real database, how a contact is actually made, and who else touches a call.

You do not need this page to use Rilo, and nothing here contradicts the simpler version. It is the same product, described precisely.

The whole database

Everything our server knows about your family.

Not a summary. These are the seventeen tables the server has, with the columns they have. Four are your family: who they are, who may act for a child, the phones they speak from, and where to ring them. Four hold something for a phone that was switched off, and all four delete themselves. Three are about who may act for a child and which phone stopped being trusted. Two are signed records of an ending: a contact cut, or somebody gone. They are kept so a phone which was offline can check them rather than believe us. One is spent one-time numbers, swept as they expire. One is the subscription, and it holds nothing that names anybody. One is a report a grown-up sent us, and it is the only table here that exists to make something happen. The last is not about your family at all.

accounts kept until you delete it
  • ida hash of the public key, not a name
  • public_keythe device key that is the account
  • typeadult or child
  • type_certthe signature proving it. A child account is signed by a parent, so there is no flag to flip
  • created_at
  • last_activethe last time any of your phones said hello
  • dormant_warned_atempty
  • consent_sealedyour sign-up email, sealed to a key that lives on paper with the founder. No server can read it
  • uncovered_sinceempty, unless your plan stopped covering this child — then the day we noticed, so the month before anything stops starts from then
Your account row stays until you delete it, and deleting it in the app takes this row and everything attached to it with it. We are working on clearing out families who have stopped using Rilo altogether; until that is switched on, nothing here goes on its own.
server_state kept
  • keywhat is being remembered
  • valueand what it says
The only table here that is not about your family at all. Today it holds one thing: the moment this version of the server first started. Every question about who has gone quiet is measured from then, so a database written before we could tell can never be read as months of silence.
guardians kept
  • account_id
  • guardian_keya key allowed to act for this child
  • added_at
  • removed_at
Removal is marked, not erased. Contacts are checked against who was a guardian when the contact was signed, so a parent leaving the account does not silently sever every friendship they ever approved.
devices kept
  • account_id
  • device_keyone phone’s own key. A person can speak from more than one
  • agreement_keythe key a message is sealed to, so only that phone can open it
  • certthe signature that let this phone in, kept word for word so your phone can check it instead of believing us
  • kinda phone, or the printed recovery sheet
  • added_at
  • removed_at
The phones and tablets one person speaks from. It says how many devices your family has and nothing whatsoever about who you talk to, no more than the two tables above it. A phone is let in by a signature from a device already on the list, never by us, and that signature is kept here so any phone can check it rather than believe the list. Retiring a phone marks the row instead of erasing it, for the same reason a grown-up leaving is marked: everything it signed while it was yours has to stay checkable.
push_tokens kept
  • account_id
  • device_keywhich of your phones this one rings
  • platformapns or fcm
  • tokenan opaque handle from Apple or Google
  • updated_at
One per device, so a call can ring a phone that is asleep. The push carries no name and no content.
used_nonces swept
  • nonce
  • expires_at
A list of one-time numbers already spent, so a captured request cannot be replayed. Deleted as it expires.
denied_edges swept
  • edge_hasha hash of the pair, not the two people
  • denied_at
  • expires_at
When a grown-up says no to an introduction, this remembers the no for three days, so the other parent’s phone, switched off at the time, can’t accidentally complete it. A fresh scan between the same families clears it early. No names, no keys: a hash and two dates.
pending_grants swept
  • edge_hasha hash of the pair, not the two people
  • grant_recordthe permission being decided, and the signatures collected so far
  • waiting_keywhose signature is still missing
  • requested_bythe phone that asked, so one phone cannot flood a family with questions
  • originscanned in person, or a parent acting for their child
  • delivered_towhich phones already have it
  • declaredthe asking side’s own name, sealed so only the family they scanned can read it
  • created_at
  • expires_at
This is the one place the server holds anything resembling a social graph, and it holds it only while an introduction is unfinished. Nobody answered in 72 hours: it lapses. Answered but the phone it was meant for never came online: 30 days. Once both phones hold the finished contact, the row is deleted. That is why there is no table of contacts on this page. There is no table of contacts.
messages deleted on delivery
  • id
  • recipient_idrouting needs this one in the clear
  • recipient_devicewhich of their phones the box is sealed for. Each phone has its own key, so each gets its own box
  • sender_idso the phone knows whose key opens it
  • sender_devicewhich of their phones sealed it, so the right copy finds the right lock
  • edge_hashthe pair, hashed, so this cannot be read as a map of who talks to whom
  • sealedciphertext. There is no text column and there never will be one
  • sent_at
  • expires_at
A phone is switched off most of the day, and a message that quietly vanishes is a broken product. So a sealed box waits here: at most 30 days, and deleted the moment the recipient confirms they have it. We cannot open one.
revocations kept
  • edge_hashthe pair, hashed
  • revoked_at
  • recordthe signed record, so a phone that was offline can verify the cut itself rather than taking our word for it
The one thing we keep indefinitely. It records that a contact was cut without being a readable list of who knew whom.
roster_changes kept
  • id
  • account_idwhose list of phones and grown-ups is changing
  • kindjoining, or being removed
  • subject_keythe phone or the grown-up it is about
  • subject_kindwhich of those two
  • agreement_keythe second key a phone needs, empty for paper
  • recordthe signature itself, kept word for word so a phone can check it instead of believing us
  • signed_bywhich key signed it
  • by_authoritywhether a recovery sheet signed it, which is what makes it wait
  • issued_atthe moment the signer claims, never our clock
  • reasonempty, unless the signer wrote one
  • proposed_at
  • effective_atwhen it becomes true, two nights after it was proposed
  • contested_at
  • contested_bywho objected, and the signature they objected with
  • contest_record
  • resolved_at
  • resolutionapplied or cancelled, once it settles
The two nights between somebody signing a change to a child’s grown-ups and that change being true. A phone joining a family, or one being thrown off, waits here where every other phone on the account can see it and object. The row stays after it settles, because a phone that was switched off has to be able to check what happened rather than take our word for it. It holds the account and the keys involved, which is more than most tables here.
roster_change_votes kept
  • change_idwhich waiting change
  • voter_keywhich grown-up
  • decisionlet it through, or stop it
  • recordtheir signature
  • at
One row per grown-up rather than a count. A majority of a child’s grown-ups can end the two-night wait early, and whether there is a majority has to be asked again against the grown-ups there are now. Somebody who left is not a vote, and a counter could not tell.
device_revocations kept
  • account_idwhose phone it was
  • device_keythe key that was stolen
  • revoked_at
  • recordthe signed proof, so a phone can check it without asking us
A stolen phone, and the difference between this and retiring one. Retiring means it stops being able to act from now, which is the right answer for a phone somebody replaced. This means distrust everything it ever signed, which other phones have to be able to check without asking us. Only theft gets a row.
retirements kept
  • account_idthe account that is gone
  • retired_at
  • recordthe signed proof, made with that account’s own key before the key was destroyed
A list of absences, and the good news is the second column. When somebody leaves Rilo, their friends’ phones are handed a signature made with that person’s own key. A family learns that somebody is gone from proof, not from us saying so. Nothing we could say on our own can take a face off your child’s screen. It is the one table here with no link back to an account, because it has to outlive the one it is about.
entitlements kept
  • account_id
  • storeapple, google, or on the house
  • store_refan opaque handle from the store. It is the only thing that lets us ask it a question later
  • store_ref_hasha fingerprint of the handle above. It is what stops one receipt covering two accounts
  • environment
  • stateactive, grace, lapsed, lapsed_trial or disabled
  • seatshow many children it covers. A number, never their names
  • expires_at
  • acts_at
  • updated_at
Nothing here identifies a person: no store account, no Apple ID, no Google account, no name, no email, no card. Which children a payment covers is worked out from the guardian table when it is asked, and never written down.
roster_notices swept
  • device_keythe phone that has not been told yet
  • change_idwhich change this is about
  • whatproposed, contested, or settled
  • account_idwhose list of phones moved. When a grown-up is told about their child, this is the child
  • subject_keythe phone that was added or removed
  • effective_at
  • created_at
  • expires_at
  • delivered_atwhen we managed to say it, or empty because we have not
A queue, and the reason adding or removing a phone happens out loud. Every phone in the family hears about it, and for a child so does every grown-up responsible for them, including the phone that was just removed. That is how somebody who should not have it finds out you noticed. A notice holds nothing the change itself does not, and it is deleted when it expires whether we managed to deliver it or not.
reports kept
  • id
  • about_keywho the report is about. Readable, so that somebody can act on it
  • reasonwhich of six things happened, as the grown-up reporting chose it
  • csa
  • sealedwhat was written, sealed on the phone that sent it with a key that lies on paper with the founder. No server can read it
  • sealed_digesta fingerprint of the sealed part, so that what is opened later is provably what arrived
  • received_at
  • edge_snapshotwho had been allowed to reach whom, and which grown-up allowed it, as it stood at that moment. This one cannot be reconstructed later
  • reporter_keywho sent it, or nothing at all when it was sent without a name
  • source
  • sealed_key
  • receiptthe reference the sender keeps. It is the only way we can answer somebody about a report we cannot open
  • acknowledged_at
  • decided_at
  • actioned_at
  • escalated_at
  • decided_by
  • outcome
The one table here that exists to make something happen. Most of a report is sealed on the phone that sent it and we cannot open it. What stays readable is who it is about, because a report nobody can act on is not a safeguard. It outlives the account it is about. Otherwise anybody could delete themselves out of one. There is no recording of any call in it, here or anywhere, because none is ever made.

Columns that do not exist

  • email
  • name
  • age
  • photo
  • message text
  • call content
  • call history
  • contact list
  • location
  • device model

Your email is checked once when you sign up and sealed in the same breath, to a key that lives on paper with the founder and has never touched a server. The law asks us to be able to prove an adult agreed. We could not read it back ourselves. Names and photos are set on each phone and handed to the other phone directly. They never reach us, which is also why you can rename Grandpa to “Opa” and we will never know. We could not show any of this to anyone if we wanted to, and we could not be made to.

This section is generated from the server’s own schema, in accounts.ts and roster-changes.ts, the two files that create the database. If they ever disagree, the files are right and this page is a bug.

Contacts

How a contact is actually made.

Somebody’s phone makes a one-time invite. The other phone reads it, scanned from a screen in the same room or opened from a link that grown-up sent. Reading it starts the introduction and, in the same moment, records the other phone’s public key, so both phones know exactly which key belongs to which person from then on. That is the part that makes a later call impossible to sit in the middle of, including for us. None of it is connected until a grown-up on the other side has said yes.

Still not a contact. A responsible adult has to sign the contact on their own phone, behind a biometric check made at that moment. What they sign is the whole thing. Who, with whom, and in which direction. Their signature is what the server checks before it will route anything. The server holds no key that can sign, so it cannot create a contact, and neither can anyone who steals the database.

When both phones hold the finished contact, the row is deleted and the contact exists only on the two devices. Cutting it later writes a signed revocation, which is the one record kept indefinitely. It is stored as a hash of the pair, so it says that a contact ended without saying who knew whom.

Who else is involved

Three companies touch a call. None of them can hear it.

Six other companies touch the service, and they are all listed in the privacy policy. Three of them are involved in getting a call to ring, and this is what each of those three can and cannot see.

  • Apple & Google

    Push notifications, so a call rings a phone that is asleep. Unavoidable on both platforms. They carry an opaque identifier and nothing else. No name, no number, no content.

  • Cloudflare

    Roughly one call in six cannot connect the two phones directly, on hotel Wi-Fi or some mobile networks, and is bounced through a relay. The relay forwards encrypted packets it cannot open, but it does see that two anonymous ids were connected, when, for how long, and from which addresses. That is metadata we do not keep, held by somebody else.

  • Us

    The table above. One small server in the Netherlands that introduces two phones and then gets out of the way.