Legal
Security Policy
Lyralog stores people's most personal data: their daily lives, health, moods and money. We take its security seriously, and we value the researchers and users who help us keep it safe.
Reporting a vulnerability
Please don't report security issues in public GitHub issues, pull requests, Discord or the in-app feedback form. Those channels are visible to others, or forwarded to shared tools.
Email lyralogai@gmail.com with:
- a description of the issue, and its impact as you understand it
- steps to reproduce: requests, app version, platform and OS version, and a proof of concept if you have one
- the affected component (see Scope)
- whether you accessed, modified or kept any data that isn't yours, and how much (see Safe harbor)
- how you'd like to be credited, if at all
What happens next
| Step | Target |
|---|---|
| We acknowledge your report | Within 3 business days |
| We confirm or rule out the issue and share an initial severity | Within 10 business days |
| We keep you updated | At least every 14 days until it's resolved |
| Fix deployed: critical (for example, cross-user data access) | Within 7 days |
| Fix deployed: high | Within 30 days |
| Fix deployed: medium / low | Within 90 days |
| Public disclosure | Coordinated with you after the fix ships. By default 90 days after your report, or sooner once the fix is live. |
The API is updated continuously, so server-side fixes reach all users once deployed. App fixes also depend on store review and on users updating. We'll tell you when an app fix is live in the stores.
We don't currently run a paid bug bounty. We're glad to credit you publicly in our release notes, with your permission.
Scope
In scope
- The Lyralog API:
https://lyralog-api.onrender.com, and any custom API domain we publish - The Lyralog iOS and Android apps: current store versions and TestFlight / Play testing builds
- Our Supabase project's configuration, as reachable with the app's public (publishable) key: data exposed through PostgREST, GraphQL, Storage or Realtime; Auth settings
- This repository: leaked secrets, and insecure CI/CD configuration
- The Lyralog landing site
Examples of what we especially want to hear about:
- reading, changing or deleting another user's entries, through any path
- bypassing JWT verification, or getting the API to act as a different user
- anything that exposes entry text, questions, answers or tokens in logs, crash reports or error messages
- prompt injection that makes the Q&A or chart features reveal another user's data, or data outside the requester's own entries
- extracting session tokens from the app without an already-compromised (jailbroken or rooted) device
Out of scope
- Vulnerabilities in Supabase, Render, OpenAI, Sentry, Expo, Apple or Google themselves. Report those to the vendor. A misconfiguration of ours on those platforms is in scope.
- Denial of service, load testing, or anything that degrades service for other users
- Social engineering, phishing, or physical attacks against our staff or users
- Attacks that need a jailbroken or rooted device, a device with a user-installed root CA, or physical access to an unlocked phone. The exception is where they reveal a flaw in our own code.
- Reports from automated scanners with no demonstrated impact, such as missing headers on API responses that serve no HTML, TLS cipher-suite preferences, or version banners
- Rate limiting and spam of features with no security impact
- Self-XSS, clickjacking on pages with no sensitive actions, and CSRF on logout
- The known design limitations listed below
Safe harbor
We won't pursue legal action against, or ask law enforcement to investigate, anyone who researches and reports in good faith under this policy. That means you:
- only access or modify your own accounts and data. Create two test accounts to test cross-user access. When you've shown a flaw exposes someone else's data, stop, and don't view, keep or change more than the minimum needed to show it
- don't degrade the service, run denial-of-service tests, or spam other users
- don't use the in-app feedback form, which posts to shared team channels, to deliver payloads
- give us reasonable time to fix the issue before disclosing it (see the timeline above)
- delete any data that isn't yours once you've reported it
If you're unsure whether something is allowed, email us first. We'll consider good-faith research under this policy to be authorized, and we won't treat it as a breach of our Terms of Service.
Supported versions
| Component | Supported |
|---|---|
| API | The currently deployed version only (continuously deployed from main) |
| iOS / Android app | The latest version in each store. The previous minor version gets fixes for critical issues only, until most users have updated. |
| Older app versions | Not supported. Please update. |
Security architecture overview
A short summary of the boundaries that protect user data, so you can judge what counts as a vulnerability. For the full audit checklist, see docs/SECURITY.md; for the design, see docs/ARCHITECTURE.md.
Phone Render Supabase (Postgres)
┌──────────────────────┐ HTTPS + ┌──────────────────────┐ ┌───────────────────────────┐
│ Lyralog app │ Bearer JWT │ FastAPI │ TLS │ log_entries │
│ · supabase-js: Auth │──────────────▶ │ 1. verify JWT │──────▶ │ RLS: user_id = │
│ only (no data API)│ │ 2. SET LOCAL ROLE │ │ app_private.uid() │
│ · tokens: Keychain/ │ │ lyralog_user │ │ FORCE RLS │
│ Keystore │ │ 3. claims → session │ │ anon/authenticated: │
│ · biometric lock │ │ 4. query │ │ no grants │
└──────────────────────┘ └──────────┬───────────┘ └───────────────────────────┘
│ ZDR, store=false
▼
OpenAI
1. Tenant isolation with Postgres row-level security, keyed on auth.uid()
All app data goes through our API. The app uses Supabase only for sign-in. The Supabase roles behind its public Data API (
anonandauthenticated) have no privileges on app tables. A direct PostgREST or GraphQL request with a valid user token is refused with "permission denied", not merely filtered.The API holds no data privileges of its own. It logs in as
lyralog_api, and for each request it opens one transaction and runs:SET LOCAL ROLE lyralog_user. This role is the only one with table privileges.set_config('request.jwt.claims', <verified claims>, true), which is transaction-scoped.
Both are discarded when the transaction ends, so no request can inherit another user's identity through a pooled connection.
Row-level security is enabled and forced on every table with user data, with
user_id = (SELECT app_private.uid())in bothUSINGandWITH CHECK.app_private.uid()is aSECURITY DEFINERwrapper around Supabase'sauth.uid(), which reads thesubclaim set above. Users can't read another user's rows, or write rows under another user's id, even if the application code has a bug.Defense in depth. Queries also filter explicitly on the verified user id. Client-chosen entry ids that collide with another user's rows are rejected (
409), never overwritten. Helper functions live in a non-exposedapp_privateschema, withEXECUTErevoked fromPUBLIC.Tenant-boundary tests run in CI against a real Postgres with the same roles. They check that user B can't read, update, delete or overwrite user A's data, and that
anon/authenticatedare denied.
2. JWT validation
Every API request except health checks needs Authorization: Bearer <Supabase access token>. The API verifies the token itself (backend/app/core/security.py):
- Signature:
- ES256 or RS256 against the project's published JWKS. Keys are cached for 10 minutes and refetched when an unknown
kidappears, to handle key rotation. - HS256 with the project's legacy secret, only while that secret is configured.
- The accepted algorithm list is pinned for each key type, so
alg: noneand algorithm-confusion attacks are rejected.
- ES256 or RS256 against the project's published JWKS. Keys are cached for 10 minutes and refetched when an unknown
- Claims:
exp,subandaudare required, and so isisswhen the project URL is configured.audandrolemust both beauthenticated, andissmust match the project. Clock-skew leeway is 10 seconds. - Identity: the user id comes only from the verified
sub. Auser_idin a request body, query string or header is ignored. - Uniform failures: every verification failure returns the same
401 {"detail":"Invalid or expired token"}. The reason isn't logged, so token contents never reach logs. - No
service_role: neither the app nor the API holds, or can use, Supabase's all-access service key or role.
3. Client-side protection: biometrics and secure storage
To be precise about what each layer does:
- Session tokens are in the iOS Keychain / Android Keystore. Access and refresh tokens are stored with
expo-secure-store. The OS encrypts them with hardware-backed keys, marks them this-device-only (never in backups or restored elsewhere), and makes them readable only after the device has been unlocked once since boot. They are never kept in plain app storage. - Biometric unlock is an app-level lock, not encryption. When it's on,
expo-local-authenticationasks for Face ID, Touch ID or a Class 3 (strong) fingerprint, with the device passcode as fallback, on launch and every return to the app. The app switcher snapshot is covered too.- The biometric check happens in the OS and secure hardware. The app only gets success or failure, and never handles biometric data.
- The lock setting is stored per user in the Keychain, so it can't be switched off by editing app files.
- The tokens are deliberately not bound to biometrics (
requireAuthentication), because that would stop background session refresh. The lock protects against someone holding your unlocked phone. It is not a defense against a compromised device.
- Offline entries that haven't synced yet are kept in the app's private SQLite database, protected by the OS's file encryption (iOS Data Protection, Android file-based encryption). They're deleted as soon as the server accepts them, and on sign-out.
- Transport: production builds talk to the API only over HTTPS (TLS terminated at Render's edge). Sign-in uses Supabase's PKCE flow, so an intercepted magic link can't be used on another device.
4. AI and telemetry
- LLM calls go to OpenAI under Zero Data Retention, with
store=false, and contain only the text needed for the task. No email, account id or IP address is included. Q&A answers must cite the user's own retrieved entries, or return the fixed fallback. - Logs and crash reports are scrubbed of entry text, questions, answers, transcripts, embeddings and tokens, at several layers (
backend/app/core/telemetry.py,mobile/services/sentry.ts).
Known limitations (by design)
These are deliberate trade-offs. We know about them, and reports about them alone are out of scope unless you show impact beyond what's described:
- No certificate pinning. Render rotates certificates automatically, so pinning a leaf certificate would break the app. Transport relies on standard TLS validation.
- The biometric lock is a UI guard, not a cryptographic gate on the tokens (see §3).
- Offline entries rely on OS-level encryption, not app-level database encryption.
- API documentation isn't public in production: the OpenAPI schema, Swagger UI and ReDoc are only served to administrators, inside the admin portal. Every data endpoint requires authentication either way.
Thank you for helping keep Lyralog and its users safe.
operator name