Architecture
Supabase handles identity. Every read and write of your data goes through the Lyralog API, a FastAPI service in a Docker container on Render. The API talks to Supabase Postgres under row-level security, and to a zero-data-retention LLM for extraction, embeddings and answers.
Select a component to see what it stores, what it can see, and what it never does.
FastAPI · Docker on Render
Stores
- Nothing: the service is stateless
Can see
- Your entries while handling your request
- Your verified user ID (the JWT sub claim)
Never
- Trusts a user ID sent in the request body
- Uses the Supabase service_role key
- Queries outside a SET LOCAL ROLE lyralog_user transaction
- Mobile app → FastAPI: HTTPS + Bearer JWT
- FastAPI → Postgres + pgvector: RLS-scoped transaction
- FastAPI → LLM provider: Extract · embed · answer
Security & Encryption
Several independent layers protect your data. Here they are, from your phone inward.
On your device
- Biometric lock. With
expo-local-authenticationthe app asks the OS for Face ID, Touch ID or fingerprint. Matching happens in the phone's secure hardware (Secure Enclave on iOS, TEE / StrongBox on Android), and the app only gets back a pass or fail. It never sees your biometric data. - Session tokens in secure storage. Your Supabase session is kept in
expo-secure-store: the iOS Keychain withAFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY(excluded from backups and never migrated to another device), or values encrypted with an Android Keystore key. - Offline queue. Entries you write offline wait in a local
expo-sqlitedatabase, scoped to your user ID, until they sync. That file is protected by the OS's file encryption rather than a separate app-level key. Settings → Clear Local Cache removes it.
Auth & transport
- All traffic uses HTTPS. The app sends your Supabase access token as
Authorization: Bearer. - The API verifies every token itself: the signature (ES256/RS256 via the project's JWKS, or legacy HS256), plus
aud = authenticated,role = authenticated, the project issuer and expiry. - Your user ID always comes from the verified token's
subclaim, never from the request body. Neither the app nor the API uses Supabase'sservice_rolekey.
Row-level security with auth.uid()
Every table holding your data, embeddings included, has Postgres RLS enabled and forced. The policy only allows rows whose user_id matches auth.uid(), the sub of the verified JWT, for reads (USING) and for writes (WITH CHECK).
-- Only lyralog_user has privileges on app tables.
ALTER TABLE log_entries ENABLE ROW LEVEL SECURITY;
ALTER TABLE log_entries FORCE ROW LEVEL SECURITY; -- applies to the owner too
-- SECURITY DEFINER wrapper: returns Supabase's auth.uid() (the JWT "sub").
CREATE FUNCTION app_private.uid() RETURNS uuid
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = ''
AS $$ SELECT auth.uid() $$;
CREATE POLICY user_isolation ON log_entries
FOR ALL TO lyralog_user
USING (user_id = (SELECT app_private.uid()))
WITH CHECK (user_id = (SELECT app_private.uid()));
-- The Data API's roles get nothing.
REVOKE ALL ON log_entries FROM anon, authenticated;The API logs in as lyralog_api, a role with no table privileges and no BYPASSRLS. Each request runs in a single transaction that switches into lyralog_user and sets the verified claims:
BEGIN;
SET LOCAL ROLE lyralog_user;
SELECT set_config('request.jwt.claims', '<verified JWT claims>', true);
-- every query for this request runs here, filtered by RLS
COMMIT; -- role and claims reset automaticallySupabase's own Data API roles (anon, authenticated) are granted nothing on app tables. Even with a valid user token, calls to PostgREST or GraphQL for your data are denied. Automated tenant-boundary tests check all of this on every pull request.
AI processing
- Extraction, embeddings and Q&A use commercial LLM APIs under a Zero Data Retention agreement: inputs and outputs are not stored by the provider or used for training. Every completion also sets
store=false. - The model only sees the entry being processed or, for a question, the entries retrieved from your own RLS-scoped data. It never receives your email or account details.
- Answers are grounded only in your entries. If the answer isn't there, the assistant replies “I don't have any logged entries regarding that.” rather than guessing.
- Charts: the model fills in a structured chart spec from an allow-list. It never writes SQL. The API compiles the spec into a parameterized query run under your RLS scope, and the app renders the result natively.
FAQ
Who owns my data?
You do. Your entries are used only to provide the service to you. They are never sold, never used for advertising, and never used to train AI models, ours or our providers'.
Can I export my data?
Yes, any time. Settings → Export My Data saves everything you have logged, including entries not yet synced, as JSON or CSV: the raw text, timestamps, tracker, category, tags, metrics and reminders.
Developers can call GET /api/logs/export with their access token. Internal embeddings are excluded because they're derived data, not yours to re-import.
Can I fix or delete an entry?
Yes. Tap Undo right after saving, or delete any entry from the last 7 days under Last 7 days on the Log tab. Its text comes back in the entry box so you can fix it and save it again, and an entry from an earlier day keeps its original date. Deleting removes the entry, its extracted details and its embedding for good.
What is stored in the cloud and what stays on my phone?
- Cloud (Supabase Postgres): synced entries, the metadata extracted from them, and their embeddings, all isolated by row-level security. Account identity lives in Supabase Auth.
- Phone: your session tokens (Keychain / Keystore), entries not yet synced, cached screens and preferences.
- Nowhere else: the LLM provider keeps nothing under zero data retention, and the API server is stateless.
Does the app work offline?
Logging does. Entries save to the on-device queue instantly and sync in the background once you're online. Each entry has an ID generated on your phone, so retries never create duplicates. Questions and charts need a connection, because they run against your full history on the server.
Can anyone at Lyralog read my entries?
The app and API are built so that no user-facing path can read across accounts: RLS is forced on every table and the service-role key is never used. As with any hosted service, database administrators have infrastructure-level access.
What if I ask about something I never logged?
You'll get “I don't have any logged entries regarding that.”. The assistant is forbidden from using general knowledge or guessing, so every answer can be traced to your own entries.