Privacy
Privacy defaults
This product listens to a child in their home. These are the defaults the system actually implements today — not aspirations. If a change would weaken any of them, it needs a deliberate decision, not a silent commit.
Standing constraint: personal/family use only until child-privacy counsel is consulted (COPPA, and provider terms on child-directed use). See the roadmap's Phase 6 checkpoint.
Audio
Never retained. Microphone audio is streamed to the gateway, forwarded to speech-to-text, and dropped. Nothing is written to disk, to Firestore, or to Cloud Storage at any point. A child's voice exists in transit, in the provider's own processing, and — for one activity, for the length of one session, in memory only — in the replay described below.
The one exception is a developer running gateway/test/* by hand, which writes WAVs to gateway/test/out/ — gitignored, on that developer's laptop, and never produced by the deployed service.
The box's own voice is a different thing, and since 2026-08-26 it is kept. Everything the toy says is our authored text — line packs and scripted engines — read aloud by the narrator. Those readings are now stored in a private Cloud Storage bucket (kids-companion-toy-speech) so a line is bought once instead of being re-bought every time the service is redeployed. It is written down here rather than left to be discovered, because the sentence above says audio is never written to Cloud Storage and a reader could reasonably take that to cover all audio. It does not: it covers the microphone, and the microphone rule is unchanged.
Three things about what is stored. Some of these lines say a child's first name out loud — "Aarav, बोलो…" — because addressing a child by name is how a room with one microphone knows whose turn it is. That name already travels to the narrator as text on every render; this is the same name, now also at rest, in audio, in our own bucket. Nothing else is stored: an object's name is the SHA-1 of the request, so the bucket holds opaque audio with no text, no child id, no household id and no session id, and there is no way to enumerate whose lines they are. Objects expire after 90 days, after which the line is simply bought again.
The trade is deliberate and it is not only money. The narrator generates a fresh reading every time it is asked, so a line re-rendered is a line re-read — the same words in a slightly different voice. Keeping the reading is what makes the box sound like one character from one week to the next.
Sound Safari holds an answer in memory, and plays it back. The band 3-5 listening game ends by replaying the safari: each animal's recording, its name, and the child's own voice saying it. So for the length of one session — five or six minutes — the gateway keeps up to two and a half seconds of each answer in RAM, and speaks it back through the toy's own speaker.
Nothing about "never retained" changes. That audio is never written to disk, never sent to Firestore or Cloud Storage, never sent to any third party beyond the speech-to-text it was already going to, and it is dropped when the session ends or is cancelled. It goes to the child who said it and nowhere else.
Two properties are enforced rather than intended. An engine must ask for a turn to be kept (capture: { maxMs }, gateway/src/turns/pipeline.js), so no other activity's microphone is held for a moment longer than the turn it belongs to; and a cancelled turn drops what it had collected. Both have tests.
Saving or sharing that recording is deliberately not built. It is an obvious next step — a twenty-second clip of a three-year-old shouting animal names is exactly what a parent sends a grandparent — and it is the point at which this section would stop being true. It needs its own decision, taken with the standing constraint above in view, not a quiet commit.
Transcripts
Not stored. A transcript exists in memory for the length of one turn, plus a short rolling conversation history (six exchanges) that lives only in the connection and dies with it. Nothing is persisted.
Not logged by default — since 2026-09-01, the day boxes started going to households that are not the owner's. The per-turn log lines carry only metadata — language, word counts, and timings. LOG_TRANSCRIPTS can add a 60-character snippet of what the child actually said, and while this product was in development (2026-08-25 → 2026-09-01) that was the deployed default, recorded honestly in this section's earlier text. It is now the typed-out debug opt-in (LOG_TRANSCRIPTS=1 ./deploy.sh), used only against the owner's own household; a plain ./deploy.sh — which is what /ship runs — deploys with transcripts off.
The history matters to this document's honesty: the flag was forward-only- when-set before 2026-08-25 (so this section overstated what was logged), then deliberately ON during development. Both defaults were chosen to keep this file describing the service that actually exists — the failure this file exists to prevent — and the flip to OFF happened before the first non-owner household, which is the order the promise required.
What the debug opt-in does not change, when it is on. Audio is still never retained, nothing a child says is written to Firestore or Cloud Storage, and the rolling conversation history still dies with the connection. It is a log-retention exposure — bounded by Cloud Logging's retention on the kids-companion-toy project — not a change in what the product stores. The standing rule that put the default at OFF stands: it is never turned on against a household that is not the maintainer's own; that is part of the personal/family-use-only constraint at the top of this file, not a separate task to remember.
Story Time listens once and keeps nothing. The story opens with a question and the box opens the microphone for a single turn. The answer is never judged and never stored: the only thing read from it is whether anyone spoke at all, which chooses between two lines that were recorded days earlier. There is no right answer to give and nothing about the answer changes the story. The stories themselves are our own authored content, written and reviewed ahead of time — nothing a child says is used to make them.
What a story does keep is its own place: which story and which chapter the box was playing when it stopped, so a bedtime story cut short can be finished the next evening instead of started over. That is a story id, a language and a chapter number on the box's own record — no audio, no words, nothing the child said — and it is erased the moment the last chapter is queued.
Story Builder keeps the story, and that means it can keep their words. This is the one place the product writes down something a child made, and it is worth being exact about, because the easy summary of it would be a little untrue.
The children and the box make up a story out loud, turn by turn. What a child says exists in memory for that turn and is never written to the record, never logged, and never sent anywhere except the speech and language provider that has to read it to answer. For the youngest children the box now offers two choices instead of asking them to invent, and a chosen option is matched on our own server: on those turns the child's words are never sent to the language model at all — the story is continued from the option text we wrote. At the end the box replays the story it told — its own recorded lines, held in memory for the session and gone when it ends; the child's voice is never part of that replay — and separately writes the whole story out as one piece of prose, and that written retelling is saved on the session record so a parent can read what their child made.
The retelling is the box's own writing, not a transcript — but the ideas in it are the children's, kept faithfully, because that is the entire point of reading it. And the box is asked to reuse a child's own words where it can, so a sentence a child invented can appear in the saved story word for word. We measured that happening and decided to keep it: a story that drops what the child actually said is not their story. So the honest promise is that we do not keep a transcript of your child — not that nothing your child said is ever written down.
Because of that, what a child says is checked before the box writes with it, not after. A contribution that is about the child's real life — their family, their home, their school, a real person — is detected in the gateway's own code and never reaches the language model at all: nothing is generated from it, nothing is spoken about it, and nothing derived from it is saved. The box does not tell the child, and does not refuse; the story simply carries on. This is a deliberately blunt rule and it will sometimes catch an innocent sentence, which costs one scripted line. We chose that over the other mistake. The safety-word classifier gates the same turn, and a generated line is checked once more before it is spoken and again before it is saved.
A session stopped part-way keeps nothing at all — there is no retelling yet, so there is nothing to keep. If the box cannot write the retelling, the record carries no story rather than a stand-in.
Scanned chapters
A parent may photograph a chapter of their child's schoolbook, and the gateway turns the pages into a bank of about forty revision questions. The box now quizzes on it too: eight questions a session, easiest first, and a second run leans toward whatever the child missed the first time rather than repeating what they already know.
The photographs are never retained. They arrive in one request, are held in memory for the length of the job, and are dropped — the same posture as microphone audio. Nothing is written to disk, to Firestore or to Cloud Storage.
Two providers see the pages, once each. The images go to Mistral for OCR; the text that comes back goes to OpenAI, which writes the questions. Neither is told anything about the child — no name, no age, no profile — only the page and, if the parent gave one, the class the book is for. Their retention policies govern that data once it leaves the gateway. Both keys live only in gateway/.env locally for now; nothing is deployed yet, and the route answers 503 when the keys are absent.
The chapter's text is not kept either. Only the derived material persists — a title, subject, medium, topics, a page count and a state, plus the questions themselves — under the family's own households/{uid}/chapters/{chid}. firestore.rules lets a parent read and delete that document and denies every client create or update, so a parent cannot hand-edit an answer key their child would later be quizzed against. A consequence worth stating plainly: because neither the photograph nor the chapter's text is stored anywhere, a chapter whose questions come out wrong has to be photographed again.
A clean answer never leaves the gateway; anything else goes to Sarvam, and only Sarvam. The box first checks what a child said against the question's own accept-list — a token match, no network call, done before the child has finished the next breath. Only a transcript that misses the list is sent anywhere, and it goes to Sarvam — the provider that has turned the child's speech into text since Phase 2, so sending a transcript to it for judging opens no new provider and no new privacy surface. The question and its one-sentence correct answer travel with it, so Sarvam can judge the meaning against them; nothing about the child does — no name, no age, no profile. No other provider ever sees a spoken answer.
What the box remembers afterwards is small on purpose. gateway/src/chapters/progress.js, called the moment a session ends (gateway/src/sessions/mirror.js's end()) — including one cut short, because a question already put to a child was still put to them — writes one document, for the chapter's owner only, at households/{uid}/chapters/{chid}/progress/{cid}: for every question that session put to that child, an ask id and two counters — how many times it has now been put to them and how many times they got it right — plus the date. It is written only by the gateway, and firestore.rules makes it read-only to the parent, the same posture as a session record. The document itself holds no transcript, no audio and no answer text — an ask id and those two counters plus a date, nothing else. What reaches Cloud Logging, separately, is only the verdict — ask a1 (explain) -> right, never the child's words. And nothing at all about a guest, who has no household profile to attach a history to and is never written — nor about a sibling playing along, whose answers on someone else's chapter are recorded nowhere at all (2026-08-19: a chapter belongs to the child it was scanned for, and only their history steers what the box asks next time). One gap worth naming: deleting the chapter removes the document a parent can see; nothing today reaches into the progress documents underneath it — no code deletes them, and the same rule that keeps a client from writing them also keeps a client from deleting them — so a child's counters currently outlive a deleted chapter as a document Firestore never actually clears.
A photograph of a page may incidentally include a child's handwritten name or notes. Nothing in the pipeline looks for it, and nothing keeps the image.
What Firestore holds
Only what the box needs to behave correctly, all of it under the family's own households/{uid} tree: children's first names, ages and language settings (including, optionally, a child's gender — asked only when the box will speak a language whose verbs and adjectives inflect for it, used only to pick the right form of a spoken line, and skippable, in which case the box uses its neutral phrasing); remembered friends (a first name and an age, nothing else — sessions spec); the box's pairing, voice and button assignments, its last-picked story theme, and — only while a story stands cut short — the resume point described above; and session records — who played (first names, ages, age bands), the scores, a per-player outcome (won/tied/lost/completed/dropped/listened), when it started and ended, and a one-line summary ("Meera won today's Word Lab"). A session record holds no transcript — nothing a child said is written down turn by turn. One activity, Story Builder, deliberately keeps the thing the session made; it is described below, and it is the only free-text field a session record carries.
Vocabulary adds one more record, deliberately (2026-08-19): per-child word progress — which of our own ladder words the box has taught this child, whether their checks were answered right, a mastered count, a level number, and the dates involved. The words are our content; only the verdict is the child's, and no speech or transcript is ever part of it. It lives under the family's own households/{uid}/children/{cid} tree, is written only by the gateway, is read-only to the parent, and is deleted with the child.
Security rules (firestore.rules) deny everything by default. A parent reads and writes only their own household; session records are read-only to the parent (written only by the gateway, so a scoreboard cannot be forged or history truncated); the catalog is read-only to any signed-in parent; the device→household routing table is invisible to clients entirely.
Deleting it all
A parent can delete the account from the app (Settings › Delete my account, since 2026-09-06). The phone proves the password again, then asks the gateway to wipe households/{uid} — every child and their progress, boxes, the people the box knows, sessions and chapters — and to release the box's routing so the next family can pair it; then the phone deletes the Firebase Auth user. The wipe runs server-side (gateway/src/account.js, POST /v1/account/delete) because firestore.rules deliberately deny a client any write to sessions and progress. Nothing is retained: there is no soft-delete, no backup copy, and no waiting period. What survives is only the speech-cache audio described under Audio, which carries no household or child identifier, and expires on its own.
Reservations
The marketing site (web/) has one form, and reserving a Jubbo is the only thing it does. It collects what a dispatch needs and stops there: a name, an email address, a mobile number, a city and a PIN code, how many boxes, and — optionally — the age bands and language of whoever will play, which are hints for planning, never a child's profile. There is no street address: that comes later, with the payment link. No IP address, no user agent, no analytics, no cookies — the site sets none, so there is no banner to dismiss.
The phone number is there for one reason, a message about dispatch, and the form says so where you type it. The city and PIN are for working out where the first batches go.
Entries land in reservations/ in the same Firestore project, keyed by a SHA-256 hash of the email, so a repeat submission updates one row instead of creating another. firestore.rules denies clients that collection outright (the catch-all default); only the site's server-side route, holding a service account, can write to it.
Reserving costs nothing and charges nothing — there is no card, so no card details exist to store. These are adults' details, not children's data, but the promise is the same: they are used to get a box to you and to tell you about it, and they are never sold. You will get a confirmation when you reserve, and we will write again when your box is ready to ship. Reply to either and it reaches a person, who can delete your reservation if you ask.
Moderation events
Logged as a verdict and a category — never the child's words. A blocked question shows up as moderation input: BLOCKED (classifier), which is enough to tune the filter without keeping a record of what a child said.
Third parties
Audio and transcripts pass through Sarvam AI (speech-to-text, language model, text-to-speech). Their retention and training policies govern that data once it leaves the gateway; the API key is per-account and lives only in Secret Manager. No other third party ever hears a child's voice, and no other third party receives a child's words. One does now receive a child's first name — see "Gemini is now on the live path" below. Providers sit behind swappable interfaces (gateway/src/providers/), so this is a decision that can be revisited without rewriting the product.
Resend delivers the email this product sends: the confirmation for a reservation made on the website, and — once it is switched over — the account mail Firebase Auth sends a parent, a password reset or an address verification. They see the recipient's address and the text of the message, and nothing else: never a child's voice, never a child's words, never a session. Mail is sent from updates.getjubbo.com, a subdomain used only for sending, kept separate from the getjubbo.com mailbox that receives the reply — so a reply reaches a person here rather than a machine there. The API key lives in the environment and never in git.
Two more providers exist on the content side, and the distinction matters: the nightly stories are written by Anthropic's claude CLI and narrated by Google's Gemini TTS — but everything they touch is our own authored text, produced on the maintainer's laptop before any child hears it. Sound Safari is narrated by the same Gemini voice and belongs in the same sentence: its whole script is rendered on that laptop and ships inside the gateway image, so the deployed service plays audio rather than asking for it. Stories are human-approved before they ship. Vocabulary word packs are not (owner decision, 2026-08-19): they are Claude-generated and machine-validated — schema checks, a banned-word list, duplicate detection, and a second model pass reviewing every entry, with refusals logged — and publish with no human gate. Nothing a child says flows to either; that pipeline is one-directional, from us to them.
The web app changes nothing here (2026-09-07)
The parent app also ships as a web app at app.getjubbo.com. It signs in to the same Firebase project, reads the same Firestore documents under the same rules, and calls the same gateway routes as the phone app — the gateway now answers those calls for that one origin, and no other. Nothing new is retained anywhere. The one genuinely new surface is on the box: during setup it serves a small web page on its own jubbo-setup network so a phone with only a browser can hand it the home WiFi name and password. That page exists only while the setup network is up, never once the box is on home WiFi, and what it stores is exactly what the phone app has always stored — the WiFi credentials, in the box's own flash.
Gemini is now on the live path too (2026-08-23)
Three sentences that used to sit in this section are no longer true, and saying so plainly matters more than the sentences did.
The deployed gateway does call Gemini. It is the box's narrator: every line the box speaks is sent to Gemini Live as text and comes back as audio. Those lines are our own authored content — line packs and scripted engine output — exactly as they were when Bulbul spoke them. (GEMINI_API_KEY was in fact already mounted on the deployed service before this change; deploy.sh lists it in REQUIRED_SECRETS. The old wording was stale, not merely overtaken.)
A composed reaction sends the child's first name. With LIVE_BRIEFS=1 — learn.vocabulary only, and off by default — the box may compose a reaction instead of reading a scripted one. To do that it is told the situation in English: "Aarav just repeated the new word back." The child's first name travels in that sentence, to Google. Every other narrator request contains only text we wrote.
What still does not travel. The composer never receives the child's words or the child's audio — it is told what happened, never what was said, which is what stops it repeating something a child said. A child's voice reaches only Sarvam, exactly as before; Gemini receives text and returns audio, in both directions.
Turning it off is one variable: LIVE_BRIEFS=0 returns the box to scripted reactions, byte for byte, with no other behavioural change. TTS_PROVIDER=sarvam goes further and takes Gemini off the live path entirely.
Secrets
No credential is ever committed. The device holds WiFi credentials and its own signed token — nothing else, because firmware can be dumped off a toy. API keys exist only in gateway/.env (gitignored) locally and in Secret Manager in the cloud.
This page is the product's own privacy document, published as it is kept in the source. It names files and settings because that is where the promises are enforced.