Development of Pomet AI Memory App by Rania BenotmanDevelopment of Pomet AI Memory App by Rania Benotman

Development of Pomet AI Memory App

Rania Benotman

Rania Benotman

Verified

Pomet: a privacy-first AI memory for the people in your life

Roles: Full-stack engineer across iOS (SwiftUI), backend (Supabase Edge Functions + Postgres), and internal admin tooling Stack: Swift / SwiftUI · TypeScript / Deno · Supabase (Postgres, pgvector, RLS, Storage) · OpenAI (GPT-4o, Whisper, Realtime, TTS) · Firebase Crashlytics · PostHog · React / Vite

The problem

A good chief-of-staff remembers the things you can't: that your colleague's daughter just started sixth form, that you promised to check in about someone's new collaboration, that the person you're meeting at 3pm is the one whose dad is an architect.
Pomet is an attempt to build that. You talk to it ("just met Lauren, she's Lucas's mum, she's an architect at her dad's firm, remind me to ask about the extension next month") and it turns unstructured speech into a structured, searchable person profile with reminders attached. Later you ask "what do I know about Lucas's mum?" and it answers, out loud, with only the part you asked for.
The hard part isn't the transcription. It's everything downstream: deciding whether an utterance is capture or recall (or both), knowing which of three Brians you meant, scheduling a nudge that arrives at the right moment, and doing all of it without the server ever being able to read a word of it.

Architecture

Three independently-deployed codebases, deliberately decoupled:
Component Stack Responsibility iOS app SwiftUI, iOS 17+ Capture, encryption, all UI, offline sync, local notification scheduling Backend Deno Edge Functions + Postgres Extraction, retrieval, embeddings, intent routing Admin SPA Vite + React + TS Metadata-only observability
Roughly 70,000 lines of Swift across 321 files, ~22,000 lines of TypeScript across 25+ Edge Functions, and 34 versioned Postgres migrations. The contract between client and server is frozen snake_case JSON, documented in a checked-in API contract file, so a feature spanning both sides can't silently drift.

The hard problems, and how I solved them

1. Client-side encryption that actually holds

The server never decrypts user payloads. Transcripts, extraction results, reminders, and the user's inner circle are sealed with AES-GCM via CryptoKit on the device before upload. Keys live in a separate table and are memory-only for the session.
This is easy to claim and hard to keep true, because every new feature wants a plaintext column for convenience. The interesting engineering was the migration path: shipping encrypted columns alongside plaintext ones, dual-writing while clients updated, then dropping the plaintext. One plaintext field survives on purpose (the display name, because Postgres has to be able to search it) and that trade-off is written down in the security audit rather than left implicit.
Row-level security is on every table in every environment, verified against four cases after each migration: cross-user read, same-user read, unauthenticated, and service-role. I also wrote the compliance artefacts, including a gitleaks-verified security audit and a zero-data-retention evidence document covering every model endpoint the product touches.

2. Making an LLM decide what kind of sentence it just heard

The single most-revisited problem in the codebase. When a user speaks, the system has to route the utterance:
capture: "Sarah's just moved to Lisbon"
recall: "where does Sarah live?"
capture + recall: "just saw Sarah, she's moved to Lisbon, remind me what her partner does?"
Early versions keyed on surface features (question marks, "who/what/where"), which failed constantly. "Do you remember Sarah moved to Lisbon" is a statement wearing a question's clothes. The fix was reframing the classifier prompt around information flow rather than keywords: is the user giving me information or asking for it? For mixed utterances, the classifier returns both spans and each half is routed to its own pipeline.
The supporting logic is deliberately paranoid about the model. Timeframe parsing accepts a known set of literals plus month-anchored patterns and degrades to null rather than throwing, so a model that hallucinates a timeframe field can't break recall. That posture is applied throughout: unrecognised model output is noise to be dropped, never trusted input.

3. A three-state save router instead of a confirmation dialog

Most capture apps ask "is this right?" after every note. That kills the product, because the whole value is that talking to it is faster than typing.
So extraction results route through exactly one of three states:
silent save: one person, no ambiguity, saved instantly with no UI at all
soft prompt: a name is missing or uncertain, or a reminder has no timeframe, so a non-blocking nudge appears
review required: multiple people detected, which blocks the save until the user reviews each profile
The rule is small and pure, which means it's exhaustively unit-tested and identical on both sides of the wire. Getting the thresholds right was the product work. An early build had a "skip review" fast path that we removed entirely once it became clear that a wrong silent save is far more expensive than one extra tap.

4. Semantic retrieval, and the bugs that only show up in conversation

Retrieval is pgvector similarity over profile embeddings, with a documented spec: return only the fields asked about, match the response modality to the query modality (voice in, voice out), and be honest about gaps rather than silent.
Two failure classes took real diagnostic work.
Embedding quality drift. Recall started degrading for profiles that had been updated rather than created. The cause was in the note-merge path: re-embedding omitted the surrounding context lines that the original extraction path included, so every merge quietly produced a weaker vector than the note it replaced. Fixed by unifying embedding-text construction and adding a self-healing backfill.
Referent stickiness. Asking "what does Kelly do?" then "and where does she live?" would bounce to a different Kelly. The client was correctly sending the resolved referent; the server was dropping it, because the prompt rule that preserved referents was scoped to explicit pronouns and follow-ups often have none at all ("and her partner?"). The lesson generalised: in a conversational system, client and server each hold half the state, and the bugs live in the seam.

5. Notifications, where the scheduler is a resource allocator

iOS allows an app 64 pending local notifications, total. Pomet competes for that budget with two systems: user-set reminders about specific people, and calendar-driven notifications (pre-meeting briefs, morning digests, post-meeting capture nudges). Naively scheduling both means the calendar layer silently evicts the reminders users explicitly asked for, which is the worst possible failure for a memory product.
I built a budget coordinator with an explicit contract: reminders always win. It only ever cancels calendar-namespaced requests, caps calendar notifications at 20 slots, and holds a safety margin free so reminders created between reconciles never hit the system limit. It can under-schedule calendar notifications; it can never evict a reminder. Every reconcile pass cancels all pending calendar requests and re-schedules from the current classified set using deterministic identifiers, which makes the whole operation idempotent and therefore safe to run on every sync.
The second notification problem was semantic. A UNNotificationRequest carries no type field, so once a notification is delivered the app can't tell a reminder from a brief from a nudge. Every row in the feed rendered with a person avatar whose initial was just the first letter of the copy, producing an avatar labelled "F" for "Fresh from your meeting?". I introduced a notification-kind model reconstructed from the identifier namespace and reminder-only payload keys, which drives a real glyph per kind. It also carries an isPostEvent flag that hides retrospective nudges from the feed until they've actually fired, because a "summarise your meeting" row listed 14 hours ahead of the meeting reads as nonsense.
Delivered notifications are captured into a local log before the system foreground wipe clears them, so the in-app feed survives the OS behaviour that would otherwise empty it.

6. Caching and offline: making the network optional

Voice capture is used in exactly the places with bad connectivity: lobbies, car parks, the walk out of a meeting. Losing a capture because the network dropped is unacceptable, so the app treats the network as optional rather than assumed.
A durable sync queue. Captures that can't complete are written to a disk-backed queue that survives app termination, distinct from the in-memory state tracking the recording currently on screen. Processing is kind-aware: a queued audio item is resolved by running the exact same pipeline an online capture runs (transcribe, extract, route to the save states above). An item is only marked done, and its audio file only deleted, once every step in that chain has actually succeeded. Any failure leaves it in a failed state for the next pass, which is triggered by network reconnect or the app becoming active.
Version-aware image caching. Profile photos and banners are cached in memory keyed by the version parameter embedded in the storage URL rather than by the URL itself. That's a small detail with a real consequence: when a user replaces their photo, the version changes and the cache misses correctly, so the new image appears immediately instead of showing a stale avatar until the process restarts.
Latency pre-warming. The realtime transcription socket is opened before the user taps record, so the connection handshake isn't sitting between the tap and the first word.

7. Observability that can't leak the thing it's observing

This is the section I'm proudest of, because analytics and crash reporting are where privacy-first products usually quietly break their own promise. Encrypting the database is pointless if a crash report ships a raw transcript in an error message, or an analytics event includes a person's name as a property value.
Analytics is allowlist-only. There is no free-form capture(name:properties:) API anywhere in the app. Every event and every property key that can leave the device is declared as a typed enum in a single file, with associated values constrained to booleans, counts, and enum labels so call sites physically cannot smuggle content into properties. The absolute rule is that field names may be sent and field values never are. A dedicated test acts as the CI gate and fails the build if any event or property key outside those closed lists ever appears.
Crash reporting is confined by a test that reads the source tree. Firebase Crashlytics gives useful non-fatal reporting, but it will happily accept whatever string you hand it. Rather than trusting discipline, I wrote a test that walks the entire source tree, finds every Firebase import line, and fails if one appears outside two explicitly sanctioned files. The blast radius for a crash-report leak is therefore two files that get careful review, instead of 321 files that get ordinary review. The same suite pins the payload sanitisation that keeps raw error text off the wire, and Firebase is never configured at all in Debug or under test, so the suite is deterministic and network-free.
Server-side telemetry follows the same contract. Edge Function events are metadata-only, and errors are reduced to a safe class token before emission: the error name or code, first whitespace-delimited token, non-alphanumerics stripped, truncated to 64 characters, never free text. This matters because error messages are exactly where transcript content and ciphertext tend to end up. Capture is fire-and-forget and soft-failing by design, scheduled off the request hot path via the edge runtime's background primitive, so telemetry can never throw into or slow down a user-facing flow.
Per-user AI cost tracking. Rather than provisioning per-user API keys, I emit standard AI-observability events tagged with the user id, carrying model name, token counts, and latency but explicitly never the prompt or completion text. The analytics platform computes spend from model and token counts alone, so there's no pricing table to hardcode and keep in sync, and per-user unit economics come for free without any prompt content leaving the boundary.

8. SwiftUI at scale, with rules that hold the line

321 Swift files stay navigable because of constraints enforced from day one:
MVVM with no exceptions. Every screen has a ViewModel; views contain no business logic.
A single design system file is the only source of colours, typography and spacing. No hardcoded visual constants anywhere.
All sizing is relative to GeometryReader. Screen-bounds lookups are banned outright, so layouts survive every device size and dynamic type setting.
1,182 unit tests cover the parts that genuinely break: intent classification, save-state routing, encryption round-trips, notification budgeting, calendar event mapping across three providers (Apple, Google, Microsoft), timeframe parsing, deep-link routing, and the two privacy gates above. On the backend, 410 tests do the same for extraction, merge logic, normalisation and retrieval.
The tests earned their keep during large redesigns. Rebuilding Home, People, Capture and Notifications against new designs was safe precisely because the logic underneath was pinned by tests while the view layer was replaced wholesale.

Skills demonstrated

Applied AI engineering. Prompt design as a debuggable artefact, intent classification, embedding pipelines and vector retrieval, LLM cost observability, and defensive parsing of model output. Most of the real work was structuring problems so a model could be reliable at them, not just capable.
Privacy and security engineering. End-to-end client-side encryption, RLS design and verification, zero-trust server architecture, allowlist-gated analytics and crash reporting enforced by CI, secret hygiene, and the compliance documentation that makes those claims auditable rather than aspirational.
Systems design under hard constraints. A 64-slot notification budget with a priority contract, idempotent reconciliation, a durable offline queue with exactly-once completion semantics, and a frozen JSON contract across three independently-deployed codebases.
Production iOS. SwiftUI at scale, strict architectural constraints, real-time audio, CryptoKit, local notification scheduling, WidgetKit, three calendar integrations, Crashlytics, and multi-environment build configuration.
Data modelling and migration. 34 forward-only migrations including live plaintext to encrypted transitions with zero downtime and backward-compatible decoders for legacy rows.
Product judgement. The decisions I'd defend hardest are the ones about when not to bother the user: removing the skip-review fast path, replacing confirmation dialogs with a three-state router, hiding post-event nudges until they're relevant, and giving user-set reminders absolute priority over anything the system generated on its own.

Delivery status

Pomet is currently in beta testing and has not yet been publicly released on the App Store.
The release infrastructure is in place ahead of launch. Three fully separated environments (development, staging, production) run on independent database projects with per-configuration build settings, so test data physically cannot reach the production database. Builds reach testers through TestFlight, with crash reporting and analytics wired into the production configuration so real stability and usage data is already flowing before launch day. Backend deploys run automatically on merge, while staging and production migrations require explicit sign-off rather than shipping on their own.
The current focus is the beta feedback loop. A significant amount of the notification, briefs, and recall work described above came directly out of real tester usage rather than a written spec, which is exactly the point of shipping to a small group first.
Like this project

What the client had to say

Rania is an absolute joy to work with. She's responsive, has great ideas, is flexible as things change, and oozes integrity. Combined with her technical abilities, she's a really great partner to have by your side as you figure things out.

Catherine Madden

Jun 12, 2026, Client

Posted Aug 15, 2026

Voice-first iOS app that turns speech into structured, searchable person profiles. AES-GCM client-side encryption so the server never reads user data, LLM intent routing for capture vs recall, pgvector semantic search, and allowlisted analytics that can't leak content. In beta.