Ready Already Readers · Institutional review
Privacy and compliance review record
Ready Already Readers is the operator behind the Heheding and Alexandria Jr. reading apps. This page is the review record for all of them: each control below is in the shipping product, with the place it is enforced and the check that would fail if it stopped being true.
01
Review the source documents
The formal notices are the binding text. Each one is listed by the question it answers, so a reviewer can go straight to the page that settles the item in front of them.
- Privacy PolicyNamed subprocessors, what leaves the product, retention, and how to file a request.
- Children's privacyWho may hold an account, what a child record contains, and what the adult controls.
- SafetyHow book content is checked and how an adult reports a finished book.
- SecurityStorage, access control, payment separation, and how to report an exposure.
- TermsThe agreement a household or an institution is accepting.
- AccessibilityAssistive-technology support and how to raise an access barrier.
02
Accounts are adult-only, and children never sign in
Only an adult can open an account. Sign-up refuses any request that does not carry an explicit adult attestation, before an account exists.
- Enforced by
- app/api/auth/signup/route.ts — the request schema requires adult_attested to be literally true, alongside a recorded age category, attestation version, and attestation source stored on the account.
- Checked by
- A request without it fails validation and returns 400. No account row and no auth user is created.
A child has no login. A reader exists only as a profile inside the adult's household record: a preferred name and learning context, with no email address, no password, and no credential of any kind.
- Enforced by
- The learner_identities table (supabase/migrations/20260729040729_household_learner_profiles.sql) has no authentication columns, and row-level security scopes every row to the adult's household.
- Checked by
- Insert and update policies require the guardian role, and changes run through security-definer functions rather than direct table writes.
Buying, deleting the account, and creating a public share link each require a second adult check at the moment of the action, not just a session from an hour ago.
- Enforced by
- lib/auth/adult-action.server.ts — a four-digit PIN mints a 15-minute capability token in a host-prefixed, http-only, same-site-strict cookie, under the separate scopes commerce, destructive, and public_share.
- Checked by
- The PIN is stored only as a scrypt hash with database constraints on the cost parameters; the migration records in its own comment that plaintext PINs are never stored.
03
Child details do not reach analytics, logs, or links
This is the invariant the product is built around, so it is enforced in more than one place and checked by tests that fail the build rather than by a policy sentence.
Product analytics accepts seventeen named events and nothing else. Every property is a fixed list of allowed values; the contract contains no free-text field anywhere.
- Enforced by
- lib/product-analytics/contracts.ts — the event name is a closed enum and every per-event property schema is strict, so an unrecognised key is rejected.
- Checked by
- test/product-event-spine.test.js
A payload carrying a child name, an email address, or a credential is refused outright rather than cleaned up afterwards.
- Enforced by
- findUnsafeAnalyticsField runs before schema parsing and throws UNSAFE_EVENT; key matching ignores case and punctuation, so childName and child-name are caught the same way.
- Checked by
- The test submits a payload containing a child name and asserts the event is refused.
Analytics never travels in a URL. Events are posted to a same-origin endpoint that stores nothing back into the page and echoes no properties.
- Enforced by
- app/api/product-events/route.ts — POST only, same-origin required, private and no-store, with a body size cap and per-visitor rate limiting.
Error reports are redacted before they leave the device, deny-by-default: child, reader, story, book, and prompt keys, plus any key ending in id, are replaced. In a URL only a fixed vocabulary of route segments survives, and query values and fragments are always replaced.
- Enforced by
- lib/observability/sentry-scrubber.ts, wired into the browser, server, and edge runtimes with default personal-data collection off and session replay set to zero.
- Checked by
- test/sentry-scrubber.test.js
Application log lines redact child name, known words, booster words, story text, prompts, and email addresses on every write.
- Enforced by
- sanitizeLogContext in lib/observability/logger.ts runs inside the log writer, so a caller cannot skip it.
Learning records keep counts and salted hashes. The child's name, the word lists, the book title, and the story text are stored as null.
- Enforced by
- recordBookLearningEvent writes only counts and hashed values into the training data table.
- Checked by
- test/learning-privacy.test.js asserts each of those columns is null.
A share link cannot leak a name. Every token of the stored name is stripped out of the slug before a 128-bit random suffix is added, and the shared record is an explicit field whitelist rather than the book row.
- Enforced by
- lib/book-shares/public-shares.ts
- Checked by
- test/public-share-privacy-and-entropy.test.js
Advertising pixels never load on the child-facing home, landing, and examples surfaces of the consumer brands, and are suppressed entirely for a Global Privacy Control signal or inside the native apps.
- Enforced by
- lib/analytics/paid-media.ts
- Checked by
- test/native-privacy-analytics.test.js
04
What the AI providers actually receive
Story, illustration, vision, and moderation features currently use OpenAI; accounts and private file storage use Supabase. Both are named in the Privacy Policy. Running npm run verify:provider-child-metadata executes ten assertions against the real production modules with synthetic child data. It makes no network call and no database call, and it fails if any raw value appears in provider metadata.
The model provider is never sent the reader's name or appearance description as an identifier. It receives a one-way hash: the literal prefix rar_ followed by 60 hexadecimal characters.
- Enforced by
- buildOpenAISafetyIdentifier in lib/openai/cost-optimization.ts — SHA-256, or HMAC-SHA256 when a salt is configured.
- Checked by
- Assertion 3 of the provider metadata proof matches the identifier against that exact shape.
The prompt cache key is scoped to the brand and the configuration, never to a child. It resolves to values of the form story:heheding:v1:primary.
- Enforced by
- buildOpenAIPromptCacheKey in lib/openai/cost-optimization.ts
- Checked by
- Assertion 4 asserts the key contains neither the supplied name nor the supplied appearance text.
Exactly four operational fields are attached to a model request: prompt cache key, prompt cache options, safety identifier, and service tier.
- Enforced by
- buildOpenAIResponsesOptions in lib/openai/cost-optimization.ts
- Checked by
- Assertion 5 checks every key of the built options object against that allowlist, so a new field fails the proof.
Story requests switch provider-side response storage off, so the generated story is not retained on the provider's side.
- Enforced by
- Both Responses call sites in lib/story/story.ts pass store: false.
- Checked by
- Assertion 8 reads the shipping source and fails if either call site loses it.
Book images are stored under a path built from the book's identifier — books/<book-id>/images — never from a name, and the upload carries only content type, upsert, and cache control.
- Enforced by
- lib/images/image-storage-layout.ts
- Checked by
- Assertions 7 and 10 pin the storage root shape and the permitted upload options.
The proof's own receipt cannot leak. It records field names, output types, and hashes of the synthetic inputs, and the accompanying test asserts the synthetic child values appear nowhere in it.
- Enforced by
- scripts/provider-child-metadata-proof.mjs
- Checked by
- test/pre-spend-proof-helpers.test.js
05
How book content is checked
Every generation passes three checks in order: a local age-appropriateness filter, a local trademark and franchise filter, and the provider's moderation model. Any hit stops the book.
- Enforced by
- lib/safety/moderation.ts
The checks are fail-closed. If the moderation service cannot be reached, generation stops and the family is told the safety review is unavailable. It does not proceed unchecked.
- Enforced by
- A missing provider key or an unreachable provider raises rather than returning a pass.
Checks run at four points: the adult's inputs before generation, the anonymous demo path, the finished story text after generation, and the text behind every illustration prompt.
- Enforced by
- app/api/books/route.ts, app/api/demo/generate-book/route.ts, app/api/internal/generate-story/route.ts, and lib/image/image.ts.
A block reports only the category. The family's own words are never echoed back into the error, the log, or the report.
- Enforced by
- The moderation module records this in its own comment and constructs its errors from the category alone.
The safety result is written onto the book: moderation identifier, model, the time it was checked, and any flagged categories.
- Enforced by
- app/api/internal/generate-story/route.ts
An adult can report a finished book in one tap from the reader. The report is idempotent per book, so repeated taps do not multiply, and it is rate limited.
- Enforced by
- lib/safety/generated-book-reports.ts and app/api/books/[id]/report/route.ts
06
What an adult can take out, and what deletion actually removes
Export is one signed-in request returning a single JSON file: the account record, every book, reader profiles, share records, ratings, and payment summaries.
- Enforced by
- app/api/account/export/route.ts and lib/privacy/account-data.ts
Payment references in the export are truncated to their last four characters. Full processor objects, stored payment methods, card details, and secrets are not exported.
- Enforced by
- The export builder truncates every processor reference and states the same limit inside the exported file.
Deletion requires an active session, the typed word DELETE, and the adult PIN, and it runs immediately inside that one request. There is no queue and no grace period.
- Enforced by
- app/api/account/delete/route.ts
Files are deleted before database rows, and it fails closed: if a single book image or PDF cannot be removed, no account record is deleted at all.
- Enforced by
- The storage sweep in lib/privacy/account-data.ts raises a storage deletion failure before any row is touched.
- Checked by
- npm run verify:privacy-export-delete
A subscription billed through an app store has to be cancelled in that store before deletion is allowed, so a closed account cannot keep billing. A subscription billed on the web is cancelled server-side first, and if that fails nothing is deleted.
- Enforced by
- lib/privacy/account-deletion-subscriptions.ts
Support messages are pseudonymised in place rather than left attached to a deleted person: the name, address, and message body are replaced and the account link is removed.
- Enforced by
- lib/privacy/account-data.ts
What is retained, in the product's own words: a minimal book credits ledger and payment summaries for accounting, fraud prevention, refunds, chargebacks, tax, and legal integrity. Child book content and generated assets are not retained in those records.
- Enforced by
- That sentence is the reason string the deletion endpoint returns, not a paraphrase written for this page.
Measurement choice honours Global Privacy Control. A GPC signal forces the sale and share opt-out on and blocks advertising-measurement consent from being granted, whether or not anyone is signed in.
- Enforced by
- app/api/privacy/sale-share-opt-out/route.ts and app/api/privacy/paid-media-consent/route.ts
07
What this record does not claim
A review is only useful if the gaps are on the page too. These are the questions this record cannot answer, stated here rather than discovered two emails into the process.
There is no third-party security certification or audit report to attach. No SOC 2 report, and no ISO 27001 certificate.
Encryption in transit and at rest, host hardening, physical security, and backup retention belong to the hosting and storage providers rather than to code in this product. The review reply points at the provider's documentation instead of restating it here.
Generated image pixels are not run through a separate image classifier. Image safety is the moderated source text, the provider's own policy enforcement, and a gentler rebuild when a prompt is refused.
A signed data processing agreement, a completed vendor questionnaire, or a district-specific addendum is handled through the review request below. It is not published here, because the answers depend on the setting.
Nothing on this page is a claim about learning outcomes. The product does not promise them and this record does not evidence them.
08
What to send with a review request
Sending these four things usually removes an entire round trip, because the reply can start from the section you already read.
Your organisation and the setting under review: a classroom, a library, a clinic, a district, or a single family.
The document, control, questionnaire, or agreement that needs a response, and which section above it maps to.
Your deadline, and the person who should receive the reply.
Whether you need a signed agreement, and which legal entity has to sign it.
09
Send the review request
One request, to a person who can answer it and change the product if your review finds something wrong.
Patrick runs human support through support@readyalready.org, and a privacy or compliance review reaches the same mailbox. Starting from this page keeps the compliance context attached to the request, so the reply begins where you left off.