All write-ups

Pistochia

Launching soon

An iPhone app for people with ADHD. You say or type what is on your mind, and it comes back as steps small enough to start, sorted by your energy that day.

My role
Design and development, on my own
Year
2026
Built with
Expo · React Native · SQLite · Supabase Edge Functions · Claude · Whisper · RevenueCat
EXPO CLIENT: OFFLINE-FIRSTSQLITESUPABASE ANONYMOUS AUTHEDGE PROXY: OPERATIONALLOWLIST, QUOTASREVENUECAT: PAYWALLCLAUDE: TASK BREAKDOWNWHISPER: VOICE INTAKEFIRST OPENOPERATION IDWORKS IN AIRPLANE MODEFAIL-CLOSED LIMITS, AI_PAUSED KILL SWITCHPROMPTS LIVE SERVER-SIDE (ops.ts)

Scroll sideways to see the whole diagram.

ArchitectureOFFLINE-FIRST CLIENT → OPERATION ALLOWLIST PROXY → CLAUDE / WHISPER

The problem

For people with ADHD the hard part is often starting, not remembering. A to-do app that stores the whole mountain turns into a list of things to feel guilty about. The unit of work has to shrink until starting feels easy, and the app has to work the second it opens.

How it is built

An offline-first Expo app on local SQLite with versioned migrations. Identity is anonymous Supabase auth, so the first brain dump happens before any sign-up screen. Every AI call goes through an edge function that works by operation: the app sends an operation name and its parameters, never a prompt and never a key. The function checks the operation against an allowlist and enforces rate limits per IP, daily quotas per install and a kill switch. Claude breaks the tasks down, and Whisper, behind the same proxy, handles voice. RevenueCat runs the paywall, and analytics stay off until the user agrees to them.

Decisions that mattered

  • The app holds no API keys

    Operation names go in and structured results come out. The API key lives in the edge function, behind a kill switch, quotas and limits that fail closed.

  • It works offline first

    Tasks live in SQLite on the phone, with real migrations. A tool for ADHD that needs a network to show your next step would fail the people it is for.

  • No sign-up before the first use

    Anonymous auth creates an identity silently. The account screen comes later, once the app has been useful.

Code from the repo

functions/anthropic-proxy/index.tsThe edge proxy: an allowlist of operations and a kill switch
// The legacy free-relay surface: any request still carrying these is either
// an outdated client or an abuse probe — reject outright.
const FORBIDDEN_KEYS = ['system', 'model', 'messages', 'maxTokens'] as const;1
 
// …
const paused = Deno.env.get('AI_PAUSED');2
if (paused === 'true' || paused === '1') {
  return jsonResponse({ error: 'ai_paused' }, 503);
}
 
// …
for (const key of FORBIDDEN_KEYS) {
  if (key in body) {
    return jsonResponse({ error: 'invalid_request' }, 400);
  }
}
 
const op = body.op;
if (typeof op !== 'string' || !(op in OPS)) {3
  return jsonResponse({ error: 'invalid_op' }, 400);
}
const config = OPS[op]!;
  1. Line 3. The app cannot send a prompt, a model or a token limit. Those live on the server, and the app can only name an operation on the allowlist.

  2. Line 6. AI_PAUSED is a kill switch: one environment variable stops all AI spending without a redeploy.

  3. Line 19. An unknown operation is rejected. The API key never exists anywhere in the app bundle.

Where it is now

The App Store material, the launch checklist, EAS builds, a promo video and pistochia.com with a waitlist are ready. iPhone first; the App Store release is next.

Next write-upMSc dissertation: explainable detection of AI-written phishing