Technical details

Purple Askepott: how it is built on caveauAI

The safety routing, retrieval, privacy engineering and evaluation behind a domestic-abuse help site, written for engineers and the people who sign off on them. A Blue Note Logic founder project, not a client engagement.

Overview

Purple Askepott has two halves. The site itself is static HTML: it works without JavaScript, loads nothing from other websites and sets no cookies on its public pages. Ask Askepott, the assistant, is a form on that site. The site's own server passes each question to caveauAI, so the visitor's browser never talks to caveauAI or to any model provider.

Inside caveauAI the question meets a crisis router before anything else, then a search that is limited to the visitor's chosen country, then the answer model, then filters that remove anything unchecked. The answer comes back with the pages it used.

The visitor's browser shows static pages with no cookies or third parties, and a Quick exit. A question and the chosen country go to the form handler on Purple Askepott's own server, which calls caveauAI server to server with a least-privilege key. In caveauAI the crisis router runs first: signs of danger lead to verified help cards from the site's helpline registry. Ordinary questions go to a country-scoped search, then Claude Haiku 4.5 on Amazon Bedrock in the EU, then post-filters that scrub unchecked numbers and redact addresses, and the visitor gets an answer page with sources and topic cards.

Diagram source
flowchart TB
    P["Visitor's browser<br/>static pages, no cookies, no third parties, Quick exit"] -- "question + country" --> F["Purple Askepott's server<br/>Ask form handler"]
    F -- "server to server, least-privilege key" --> K
    subgraph CAI["caveauAI"]
        K{"Crisis router"} -- "ordinary question" --> S["Country-scoped search"]
        S --> L["Claude Haiku 4.5 on Amazon Bedrock, EU"]
        L --> O["Post-filters: number scrub, address redaction"]
    end
    K -- "signs of danger" --> H["Verified help cards<br/>from the site's helpline registry"]
    O -- "answer + sources" --> R["Answer page<br/>with sources and topic cards"]
Figure 1. The system: the static site, its server-side form, and the caveauAI pipeline behind it.

The site and its helpline registry

  • No third parties in the browser. Fonts, images and the few small scripts are served by the site itself. A strict Content Security Policy forbids inline scripts and styles, and public pages may not open any connection.
  • No tracking. No analytics and no cookies on public pages. Links to helplines send no referrer, so a service never learns where a visitor came from.
  • Works without JavaScript. Every page, including the assistant, is a plain form and plain links.
  • Quick exit. A high-contrast link on every page goes straight to a weather site, and a separate page explains how to hide the visit.

Phone numbers come from one verified list

A wrong number is the most dangerous mistake a help site can make, so no page holds a typed phone number. Pages refer to entries in one registry, each checked against the service's own official source. The build refuses an unknown or unverified entry, the tests refuse a number typed into a page, and no crisis-centre street address is ever published.

Each helpline is checked against its own official source and recorded in one verified registry. Pages use registry tokens instead of typed numbers. The build and tests refuse unknown or unverified tokens and any number typed into a page; when every token is verified the pages are published, and a scheduled health check re-tests the registry's links.

Diagram source
flowchart TB
    V["Each helpline checked against its own official source"] --> J["One verified registry"]
    J --> T["Pages use registry tokens, never typed numbers"]
    T --> B{"Build and tests"}
    B -- "unknown or unverified token,<br/>or a number typed into a page" --> N["Refused: nothing ships"]
    B -- "every token verified" --> OK["Pages published"]
    OK --> H["A scheduled health check re-tests the registry's links"]
Figure 2. How the build keeps phone numbers correct.

Ask Askepott: the request path

The site calls caveauAI with a key that can reach one endpoint and nothing else: it cannot list, read or delete documents. With the question it sends the chosen country and a one-way visitor key, never the visitor's IP address.

The crisis router runs first. It is rule-based, not a model, so it behaves the same way every time. It has rules in English, Norwegian and, in beta, Irish. When a question shows signs of immediate danger, the visitor gets the verified local services for their country, instead of or before any AI answer. On the hardest routes no model is called at all.

Ordinary questions go to retrieval and then to Claude Haiku 4.5 on Amazon Bedrock in the EU, with no fallback to another model provider. The model is instructed to answer in plain language from the passages it is given. After generation, any phone number not in the verified registry is removed, and street addresses are redacted.

The visitor sends a question and a chosen country to the site. The site sends the question, the country and a one-way visitor key to caveauAI, which applies the misuse limit by that key and then runs the crisis router. With signs of danger it returns a route only and calls no model, and the visitor sees verified local help for that country. Otherwise caveauAI searches only that country's sources, sends the question and the selected passages to the model on Bedrock in the EU, removes unchecked numbers and redacts addresses in the draft, and returns the answer and its sources, which the visitor sees with topic cards.

Diagram source
sequenceDiagram
    autonumber
    participant V as Visitor
    participant S as The site
    participant C as caveauAI
    participant M as Model (Bedrock, EU)
    V->>S: Question and chosen country
    S->>C: Question, country, one-way visitor key
    Note over C: Misuse limit by the one-way key, then the crisis router
    alt Signs of danger
        C-->>S: Route only, no model call
        S-->>V: Verified local help for that country
    else Ordinary question
        Note over C: Search only that country's sources
        C->>M: Question and the selected passages
        M-->>C: Draft answer
        Note over C: Remove unchecked numbers, redact addresses
        C-->>S: Answer and its sources
        S-->>V: Answer, sources and topic cards
    end
Figure 3. One question, start to finish.

Retrieval

Country scoping is a filter, not a ranking signal. Before anything is ranked, the search is limited to an allow-list of documents tagged for the chosen country or as international. A Norwegian question can never be answered from an Irish service.

Search is hybrid, combining meaning and keywords, and it starts from a wider candidate pool than the model will see. Caps per page and per site keep one website from crowding out the rest. When one site still fills the pool, a second crowd pass searches the other sites using official help terms. That pass is how an English question about Norway reaches official Norwegian sources rather than only the site's own summaries. Only a few passages go to the model, so the evaluation scores what the model actually sees, not a long result list.

The question and chosen country first pass an allow-list of documents tagged for that country or international. Hybrid search over meaning and keywords draws a wider candidate pool, and diversity caps per page and per site are applied. If one site still fills the pool, a crowd pass searches the other sites using official help terms. A few passages then go to the model.

Diagram source
flowchart TB
    A["Question + chosen country"] --> B["Allow-list: only documents tagged<br/>for that country, or international"]
    B --> C["Hybrid search: meaning + keywords<br/>over a wider candidate pool"]
    C --> D["Diversity caps per page<br/>and per site"]
    D --> E{"One site still<br/>fills the pool?"}
    E -- "yes" --> F["Crowd pass: search the other sites,<br/>using official help terms"]
    E -- "no" --> G["A few passages<br/>go to the model"]
    F --> G
Figure 4. From question to the few passages the model sees.

Privacy engineering

  • Private query mode. Retrieval for this site runs without caveauAI's query cache, and question text is kept out of logs.
  • No IP address leaves the site. Misuse is limited by counting questions against a one-way key, and the count is deleted after an hour.
  • The model provider keeps no prompts. Answers are generated on Amazon Bedrock in the EU, with prompt retention off and no fallback route.
  • One verified path. Every question takes the same no-log route. Knowledge the assistant needs is brought into the site's own corpus, so a question never crosses into another system to find it.

We test the promise, not just the design. On launch day our own check found that a search cache and an error log could still hold words from questions. Both were fixed and cleared the same evening. After any change to the assistant we now send a test question and look for it in the production logs.

Knowledge and ingestion

  • Public help only. The sources are help pages and official guidance from each country: helplines, crisis-centre networks, health services, the police, legal aid and welfare agencies.
  • An honest crawler. The crawler identifies itself truthfully. A site that refuses it is left out, never tricked.
  • Scrubbed at ingest. Street addresses are removed before text enters the corpus, so a shelter's location can never surface in an answer.
  • Tagged by country. Every source carries a country tag, and sections written in other languages are left out at query time.
  • Site furniture filtered out. Cookie notices, job adverts, donation and contact pages are filtered out so they do not crowd the help content.
  • Copied in, then re-embedded. Where useful help pages already existed in another curated collection, they were copied into this site's corpus and re-embedded there, rather than searched live in place.

Keeping the facts true

Help by topic lists services by country under eight topics: money, a place to live, legal help, police and protection, children, residence permits, health, and groups. Each service was checked against its own official page, and the exact passage it was checked against is kept with it.

A monthly automatic check fetches every official page again and flags any that have changed or can no longer be read. A person then re-checks the flagged pages. A separate check re-tests the helpline links. When a national helpline closed during the build, its pages were taken out of the assistant's sources, and the assistant was told not to suggest it.

Evaluation

A gold set of about 50 questions covers the topics and countries. Each question records the page a good answer should use. Every retrieval change is run against the set, first on the staged code and then on the live system. The measures:

  • whether the right page is among the top six search results;
  • whether any passage comes from the wrong country;
  • whether every source in the answer has a working link;
  • whether an answer carries a safety flag, such as an unchecked phone number or an address;
  • whether at least one official outside source reaches the answer, rather than only the site's own pages.

The before-and-after figures are in the Estimate box on the case-study page. They are our own measurements, not an independent evaluation.

Built, not yet switched on

Not yet switched on Everything in this section has passed its tests and stays off until it is ready to open.

Members

A member gives an email address and ticks to say they are 18 or over, nothing more. The address is encrypted at rest and found by a one-way index; it is never shown, not even on the member's own page. Sign-in uses a short code that is stored only as a one-way hash and expires within minutes. One cookie, scoped to the member pages and announced on them, keeps the member signed in, with an idle sign-out. Quick exit signs the member out as it leaves. A member can download everything kept about them, or delete the account at once. Accounts nobody uses for two years are deleted.

My notebook

The notebook is encrypted end to end in the member's own browser with the standard Web Crypto API. A passphrase only the member knows becomes a key through PBKDF2-SHA-256 and HKDF. The text is sealed with AES-GCM under a fresh random nonce for each save. The server keeps the sealed text, the dates and the last two versions. It cannot read them and cannot reset the passphrase. A notebook nobody opens for 36 months is deleted.

In the member's browser, the passphrase goes through PBKDF2-SHA-256 and HKDF to an AES-GCM key that never leaves the browser, and the notebook text is encrypted with a fresh random nonce. The server keeps only the ciphertext and dates; read back, the text is decrypted in the browser.

Diagram source
flowchart TB
    subgraph BROWSER["In the member's browser"]
        direction TB
        P["Passphrase, known only to the member"] --> K1["PBKDF2-SHA-256"]
        K1 --> K2["HKDF"]
        K2 --> K3["AES-GCM key, never leaves the browser"]
        N["Notebook text"] --> E["Encrypt with a fresh random nonce"]
        K3 --> E
    end
    subgraph SERVER["On the server"]
        SV["Ciphertext and dates only"]
    end
    E --> SV
    SV -. "read back, decrypted in the browser" .-> N
Figure 5. The notebook: encrypted in the browser, opaque to the server.

The weekly update

The update is off until a member chooses it. People write it from what is already on the site, so nothing in it is about the reader. A send log prevents duplicates, an address that bounces is paused, and every email has a one-click stop.

Creator Studio

Creator Studio publishes to Purple Askepott's own channels. Videos play from the site's own player, never a platform embed. Every post needs approval with an authenticator code, and the approval is locked to the exact version approved, so an edit needs a new approval. A global switch blocks all posting.

A draft is approved with an authenticator code, and the approval is locked to that exact version. Editing an approved post sends it back to draft for a new approval. An approved post is queued and published only while the global switch is on; while it is off, nothing posts.

Diagram source
stateDiagram-v2
    [*] --> Draft
    Draft --> Approved: authenticator code, locked to this exact version
    Approved --> Draft: edited, so a new approval is needed
    Approved --> Queued
    Queued --> Published: global switch on
    Queued --> Queued: global switch off, nothing posts
    Published --> [*]
Figure 6. A post's path through Creator Studio.

Comment insights

Comment insights reads public comments on those channels, to learn what people ask and to reach anyone who needs help. Before anything is kept, @handles, phone numbers, links, email and street addresses are removed, and the account becomes a one-way code. Everything is deleted after 28 days. The only thing kept longer is the questions people ask, rewritten in our own words and never attributed. Comments that may need help go to a separate queue.

How changes ship

The work was done by AI coding agents, Claude Code and Codex, each on a branch started from the current main, so no agent overwrites another's work. Tests run on a clean copy of the commit, not the agent's working folder. A person approves every release. Each deploy takes a snapshot first, and the live checks include the privacy canary.

An agent works on a branch started from the current main. Tests run on a clean copy with build checks, and a person approves the release. The deploy takes a snapshot first, then live checks and the privacy canary run. A regression rolls back to the snapshot; when all is clear, the evaluation is re-run.

Diagram source
flowchart TB
    W["An agent works on a branch from the current main"] --> CT["Tests on a clean copy, plus build checks"]
    CT --> H["A person approves the release"]
    H --> DP["Deploy: snapshot first"]
    DP --> LV{"Live checks and the privacy canary"}
    LV -- "regression" --> RB["Roll back to the snapshot"]
    LV -- "all clear" --> EV["Re-run the evaluation"]
Figure 7. The release loop.

What we leave out, and why

This page leaves out a lot on purpose: server names and addresses, internal paths and identifiers, keys and the names of configuration switches, rate limits, the crisis router's rule lists, and cryptographic parameters. None of them would help you judge the design. Some would help someone attack the site, or word a question to slip past the safety check.

Engraved illustration: a cracked glass slipper rests in ashes, blue flowers grow through the crack, and an open chain lies beside it.
“Life after the slipper”The bookplate on Purple Askepott's stories page. AI-generated illustration (Codex image generation), from purpleaskepott.com.