No ads, no trackers, location-stripped photos, zero social surface, zero upsells, human-reviewed content — every time, automatically.
Safety & privacy
Currently is a safe first personal website for a kid: they answer a few questions, pick five content blocks, and get a live page at their own subdomain. Parents hold the account; there are no comments, followers, feeds, or trackers. This is the architecture and the safety engineering behind it.
The design goal is one a cautious parent can check in a sentence: nothing a child writes, uploads, or links reaches the internet without passing an automated safety review first — and the system is built so that rule cannot be bypassed, including by the child who owns the page. Everything below serves that goal.
Ten rules sit above the code. Where a decision is ambiguous, these resolve it; when two conflict, safety beats delight and delight beats growth.
Deliberately boring, edge-hosted infrastructure — chosen because “boring and inspectable” is itself a parent-facing safety feature.
| Concern | Choice | Why it matters for safety |
|---|---|---|
| Compute | Cloudflare Workers (Next.js 15 via OpenNext) | Runs at the edge; no origin server to harden or leak. |
| Database | Cloudflare D1 (SQLite) + Drizzle | All access funnels through one authorization layer (§03). |
| Edge cache | Workers KV | Only ever holds already-moderated, published config. |
| Media | Cloudflare Images (+ Queues) | Strips EXIF/GPS on ingest; moderated before it’s viewable. |
| AI review | Claude (pinned haiku-4-5 snapshot) | The moderation gate for all text and images. |
| Human check | Cloudflare Turnstile | Bot protection on the one credential-redemption page. |
| Analytics | Cloudflare Web Analytics | Cookieless. No Google Analytics, no Meta pixel, ever. |
Safety is not a single check — it’s a series of independent layers, each of which fails closed. A weakness in one is caught by the next.
No route, page, or job talks to the database directly. Every read and write passes through a single authorization layer that takes the caller’s scope — parent, child, public renderer, or system — and refuses anything out of scope. The public renderer can read only published config and nothing else.
This isn’t a convention that can drift: a direct database import anywhere else is a build failure, enforced three ways — an ESLint rule, a custom boundary script, and a check in continuous integration.
db/authorized.ts · npm run check:boundary · CI
Every write path — intake answers, every block save, slugs, titles, captions, external titles — runs through an AI moderation gate before anything is stored as servable. It screens for sexual content, violence, self-harm signals, bullying, hate, drugs/alcohol, and personal information (real names, schools, addresses, phone numbers, identity-revealing handles, sub-city geography).
Hard invariants live in code, not just the prompt: the gate runs at temperature 0 with a pinned, dated model snapshot; it fails closed (an outage never approves — content simply stays unpublished); self-harm signals are quietly routed to a parent, never shown to the child as a rejection; and the child’s text is passed as data with injection-style manipulation neutralized. Rejected drafts are stored but never served back, even to their author.
The gate is regression-tested against a 110-case adversarial “golden set” that must pass 100% on the safety tier before any change to the model or prompt ships.
lib/moderation/gate.ts · prompts/moderation-gate.ts · tests/golden-moderation/
There are no third-party iframes or embedded players anywhere on a child’s page — an embed would import unmoderated adjacent content and third-party tracking. Instead, a pasted or searched YouTube video / Open Library book becomes a static thumbnail card that opens the source in a new tab.
The title runs through the text gate and the thumbnail through an image gate; thumbnail hosts are pinned (only i.ytimg.com and covers.openlibrary.org). For “find my song,” the system searches, runs each candidate through both gates, and shows the child only the ones that pass — so she picks from pre-checked options and nothing unreviewed is ever displayed.
lib/external-refs/ · pinned hosts enforced in schema, CSP, and the serving rule together
Photos upload directly to Cloudflare Images, which strips EXIF/GPS metadata on ingest. The image lands as pending and is reviewed by an AI vision gate on a background queue. Approved images become referenceable; rejected ones are deleted from storage. No image whose status isn’t approved is ever served to any surface — the editor shows a “checking your photo” placeholder instead, so no viewable URL for unapproved media can exist client-side.
lib/moderation/vision.ts · workers/queues/
Children never create accounts and never give an email, password, or phone number. Access is a short-lived signed token (currently 14 days, revocable), stored only as a SHA-256 hash, scoped to exactly one site. The redemption page is gated by a Turnstile human check; after that, a re-verified HttpOnly cookie carries the session. Revoking a token kills the session on the next action.
db/authorized.ts (kid tokens) · lib/turnstile.ts
Every response carries a strict, nonce-based Content-Security-Policy: scripts come only from our own origin with a per-request nonce — no inline scripts, no third-party script hosts, framing denied. Images are restricted to the pinned hosts. Alongside it: HSTS, X-Content-Type-Options, a strict Referrer-Policy, a Permissions-Policy denying camera/mic/geolocation, and X-Frame-Options: DENY. Kid subdomains are noindex to search engines by default.
lib/security-headers.ts · middleware.ts
Behind the currently.site zone: TLS Full (strict), a rate-limiting rule on the moderation-spending write paths, Cloudflare’s managed WAF ruleset, and Bot Fight Mode. In-worker per-child and per-IP rate limiters add a second, always-on layer for gate-spending actions, uploads, and token redemption.
lib/rate-limit.ts · Cloudflare zone config
No personal information is ever written to logs — enforced by a lint check in the deploy chain and CI that scans the dynamic parts of every log call. Intake transcripts auto-purge 30 days after completion via a scheduled job. Deleting a child is a true cascade: site, blocks, media (including the Cloudflare Images objects), external references, tokens, transcripts, and Friend Locker entries are removed, and moderation-log rows are anonymized — verified by an automated zero-residue test.
npm run check:logs · deleteKidCascade · transcript-purge cron
This is architectural, not a setting. There is no code path for comments, likes, follows, direct messages, guestbooks, visitor-visible counters, directories, search, or “discovery” — and by rule there never will be. A page is shared by its owner handing out a link, off platform. A child’s page is unlisted by default and invisible to search engines. This removes the entire class of risks that come from strangers reaching a child.
If it isn’t needed to render the page or bill the parent, it isn’t here.
| Collected | Purpose | Notes |
|---|---|---|
| Parent email | Account owner / consent | The only contact address in the system. |
| Child nickname | Display name on the page | Moderated; never a full name. |
| Child birth year | Age logic (COPPA) | Year only; never rendered. |
| Child-authored content | The page itself | Every field moderated. |
| Uploaded images | The page itself | EXIF/GPS stripped; moderated. |
| Intake transcript | Building the first page | Auto-deleted after 30 days. |
No behavioral tracking. No third-party advertising. Cookieless analytics only. No child email, password, or phone number is collected — ever.
The test suite runs against a real local database and cache (not mocks), and covers the authorization scope matrix, the deletion cascade’s zero-residue guarantee, the moderation gate’s invariants, and every content schema. The no-direct-database boundary and the no-PII log rule both run in continuous integration on every change.
Rejected or unreviewed content is never served back, to anyone, including the child who wrote it. A child’s typed draft survives only in her open form on her own device; the moment it would become viewable to anyone else, it must have passed the gate. Offline edits replay through the same save path, so even they can’t skip review.
Credibility means being clear about the current stage. Currently is in a friends-and-family pilot with informal parental consent from families known personally. The following are designed and specified but belong to the next phase, before any stranger can sign up or any money changes hands:
Nothing in this list weakens the safety posture of the live pilot — they are the pieces that let it scale beyond people the founder knows, with a lawyer’s sign-off, which is a gate the architecture above is specifically designed to make straightforward.