Remember Me

Technical notes

Under the hood.

This page is for the technical reader. It describes how the system is put together and why. The non-technical reader will find everything they need on the main page.

The one design decision that shapes everything else

The backend holds the memories and does all of the finding. When a question comes in, the backend works out what it is asking for — a time window, a named thing, or both — matches that against the database, and only then writes the answer as one sentence. The step that understands a sentence never sees the user's stored memories in order to decide which one is relevant; that decision is made against the database, in ordinary queries.

Two things follow from that. The cost of answering a question doesn't grow with the number of memories a user has. And the understanding step sits behind a single boundary, so it can be improved later without the behaviour of the rest of the system changing. How that step works is deliberately not described here.

Architecture

  1. Mobile app on the phone. Screens, voice capture, an on-device SQLite database for local-only memories, and the scheduling of notifications. The only part the user sees.
  2. Backend API — a single Node.js process. Accounts, cloud memories, money records, and all of the logic that decides which memory answers a question. A scheduled job wakes every minute to send due reminders; it takes a database advisory lock first so that multiple instances can never send one twice.
  3. PostgreSQL, managed. Only the backend talks to it. A memory has a fixed, indexed part (owner, reminder time, deadline) and a free-form JSONB part, so a memory can hold any set of fields without a migration.
  4. External services — an email service for verification and password-reset messages, and the phone platform's notification service. Both reached only from the backend, never from the phone, so no key ships inside the app.

Every request carries a signed JWT. The backend checks it, loads the user, and refuses if the token was issued before the last password reset. Every external call has a timeout. Repeated login attempts on one account are limited regardless of where they come from.

Stack, and why each piece

LayerChoiceWhy this one
Mobile appReact Native + Expo, TypeScriptOne codebase for iOS and Android. Over-the-air updates meant the phone could be updated during development without a rebuild.
BackendNode.js + Fastify, TypeScriptSame language both sides, so API types are written once. Fastify over Express for built-in schema validation and speed per request.
Database accessPrisma ORMTyped access; migrations generated from the schema, so data shape and code can't drift apart silently.
Cloud databasePostgreSQLRelational for the indexed part, JSONB for the free-form part.
On-device databaseSQLite via expo-sqliteAlready on both platforms, needs no server. Holds local-only memories.
Voice to textexpo-speech-recognition, on deviceAudio never leaves the phone; only the transcript is sent.
Notificationsexpo-notifications, scheduled locallyFire offline, cost nothing to send.
AuthenticationCustom JWT + bcryptNo third party holds the user list.

Three small decisions that turned out to matter

category and kind are two columns

Category is free text the application writes for the user to read — "travel", "boiler" — and nothing branches on it; even a card's colour is a hash of the string. Kind is a closed set the code does branch on: place, credential, medical, other. They were one column once, and a category containing "workplace" got matched as a place.

Money is stored as integers

Every amount is in minor units — cents — as an integer, never a float. Adding ordinary prices as floats eventually produces a total a fraction of a cent wrong, and in a budget that's visible.

The phone stores; the backend understands

Understanding a sentence stores nothing. The result goes back to the phone, which stores it — locally or by calling the backend. If the understanding needs a clarifying question, the memory must not already exist.