Overview
Temphera puts a journal, a planner and a money tracker in one app. People write daily entries with guided prompts, log moods, run morning, evening and weekly-review routines, track habits and streaks, and set goals for the year, the month and the day. The finance side covers expenses, income, budgets, spending limits and investments. The app is free to start, with two subscriptions on top: Plus unlocks unlimited history, insights and data export, and Pro adds couples mode, where two linked accounts share budgets, expenses and savings and settle up, and investments tracked by ticker and priced from Yahoo Finance or AMFI's daily mutual-fund NAVs.
The second half is the console the team uses to run it: support tickets, moderation, broadcasts, feature flags, analytics and system health, in a separate service with its own backend. It is built around one rule: staff work from metadata and counts, never from what a user wrote.
Temphera is our own product: co-founder Mayur Srivatsav led the build between January and August 2026 across four repositories. It has not been released: the app still carries React Native's default bundle identifiers, the store billing keys are empty, the SwiftUI home-screen widget is written but not yet added to the iOS build, and the domain is not serving.
How does the console keep staff out of user content?
The admin backend's README states its first rule plainly: no content-field access, ever. We enforced it in layers, so that no single mistake opens it up.
Reads go through allowlists
The admin service does not import the app's data models. Every user collection it reads is registered with an explicit field allowlist, and queries run with that projection built in. A user's profile in the console shows metadata and per-collection counts (journal entries, mood entries, expenses) with no way to open an entry. Even a user's free-text bio is left out. A unit test walks the whole registry and fails if any field name matches the pattern for content.
Writes go through a signed bridge
The console's database role can read user collections but can only write to its own. When staff remove a reported post, the console calls an HMAC-signed internal endpoint on the app's backend, which hides the content and can restore it if an appeal succeeds.
Moderation without the text
A moderation item carries a pointer to the content (collection, document and field), the report reason and classifier scores. The body is never rendered. An appeal must be reviewed by a different moderator from the one who made the decision, and that is checked on the server, not just hidden in the interface.
Analytics from counters
Retention cohorts, feature adoption and audience segments are built from activity counters and allow-listed metadata fields, so growth work never needs content either.
Why can't staff rewrite the audit log?
Every staff action that changes something is written through one service. A writer takes a Redis lock, reads the last entry and inserts the next one with a sequence number and the previous entry's hash. The new hash is SHA-256 over a canonical (RFC 8785) JSON form of the entry, so the same content always hashes the same way. Unique indexes on the sequence and the hash turn any race past the lock into a retry rather than a fork in the chain.
Insert-only, twice over
No code path updates or deletes an entry, and the database role for the admin service is granted insert and read on the log but not update or remove. There is deliberately no expiry index: entries are kept for at least six years.
Proof that leaves the building
Each night the previous day's hashes are rolled into a Merkle root, signed, and shipped to Backblaze B2 under a compliance-mode object lock with ten-year retention, so even someone with full database access cannot rewrite history unnoticed. Three failed shipments in a row raise a critical alert. A verifier in the console re-hashes any range and lists the daily roots, and a broken chain puts a SEV-1 banner across the dashboard.
High-risk actions
Crypto-shred, feature-flag kill switches and break-glass grants need a fresh 2FA code tied to the same person and session. A break-glass grant adds temporary permissions from a template, needs a written reason, expires on a hard deadline and asks for a post-incident note afterwards.
A review of our own code found that the fresh-2FA check on break-glass grants had been mounted as the middleware factory instead of being called, which silently skipped it. It is fixed, with a comment at the route explaining why.
When is crypto-shred allowed to run?
A deletion in Temphera is meant to be final, so most of the code is about when it is allowed to run.
Two phases
- Shred. After a 7-day grace window, the user's encryption key record is destroyed: the wrapped data key, nonce, salt and recovery material are cleared, which is meant to leave anything encrypted under that key unreadable by anyone, including us.
- Hard delete. Thirty days after the shred, the user's records are dropped from every listed collection, from journal and mood entries to budgets, habits and routines. The user document becomes a tombstone so audit-log and ticket references don't point at nothing.
Four refusals
Each phase refuses to run if the grace window hasn't passed, if the account is under legal hold, if (for the hard delete) the shred hasn't happened, or if the user has a paid subscription with no refund decision recorded. That last rule exists because the shred destroys the evidence: once the key is gone nobody can establish what the user had or was owed, so the money question has to be answered first.
In the console, scheduling a shred needs a legal basis, a reason of at least 50 characters, two acknowledgements and a fresh 2FA code, and every step is logged at SEV-1.
The honest caveat
The shred path is built and unit-tested in the admin backend. The per-user encryption it acts on is not in place in the app's backend yet; a separate piece of work owns it. Until that lands the shred step has no key to destroy, and the 30-day hard delete is what removes the data. The project's own deployment checklist marks crypto-shred as not signed off for exactly this reason, and so do we.