I used to track my finances in an app. Salary, loans, small transfers, all of it. At some point I got curious whether the team behind it could actu...
For further actions, you may consider blocking this person and/or reporting abuse
Strong agreement on the threat model — "data should be inaccessible because the team technically cannot read it, not because they promise to behave" is exactly the right framing. The two-tables-with-
localOnlypattern is clean.One angle that pushes it further: if the sync target is just another device on the same LAN (phone ↔ laptop, two phones on the same wifi), you can drop the sync engine entirely and run an embedded HTTP server inside the app. The server stays local, never opens a port to the internet, and the only thing that leaves the device is a response to a request from a device that already knows the LAN IP.
We took that route building Background Camera RemoteStream — a recording app that streams the live camera feed to a browser on the same network. No PowerSync, no encrypted sync table, no cloud backend at all. The ciphertext doesn't need encryption at rest because it never gets handed to anyone we don't control: play.google.com/store/apps/details...
Honest tradeoff: this only works when "another device on the LAN" is an acceptable sync target. The moment users need device-to-device sync across cellular networks, PowerSync + your two-table pattern is the right move and the LAN-only model breaks.
Quick question on the encryption side: how are you handling key rotation when a user changes their master password? Re-encrypting every row in
accounts_encrypt/transactions_encryptseems expensive — do you wrap a data key with the password-derived key and just re-wrap the data key, or actually re-encrypt the ciphertext?Thanks, that’s a fair distinction.
LAN-only sync is a valid model when “same Wi-Fi only” is acceptable.
On key rotation: yes, this is the KEK/DEK approach described in the “Keys” section. The domain data is encrypted with a DEK, and the DEK is wrapped with a KEK derived from the master password.
So when the master password changes, we only re-wrap the DEK with the new KEK. We do not re-encrypt every row.
The part I like most here is that you call out what stays visible. A lot of E2EE writeups stop at “server can’t read the fields”, but the remaining metadata model is where the real product tradeoffs live: dates, org IDs, row relationships, sync timing, deleted/created patterns, etc.
For finance data, even unencrypted relations can say quite a bit. A burst of rows every payday, recurring vendor-shaped records, or org membership changes may not reveal amounts, but they still leak behavior. Not saying that makes the design wrong — it’s usually the practical split — just that it’s worth documenting as explicitly as the key-recovery downside.
We’re wrestling with a similar shape at privacy.fish for mail: keep more state local, reduce what the provider can retain/read, but be honest that routing metadata and operational logs do not disappear by magic. The hard part is making the privacy boundary understandable without turning every feature into a threat-model lecture.
Same observation hits the camera-app side. Even if you encrypt the video files, the upload-burst pattern leaks "someone's home / someone just left" — bursts at 6pm, dead at 9am, that's a routine. Cloud-relay security cameras pretend the privacy story stops at "AES-256 in transit/at rest," but the metadata model (file count per hour, average size, geolocated POP) is a behavior log.
The way Background Camera RemoteStream sidesteps this is by structurally lacking the relay: footage is stored on the phone, viewed over LAN through the device's own embedded web server. There is no upstream traffic shape to analyze because there is no upstream. The cost of that design is exactly the one you're naming — it has to be understandable without a threat-model lecture. Our short form is "if your Wi-Fi is off, the data can't leave the building." Users get that one.
What we still leak: the local recording schedule itself (file-system timestamps on the device, if someone has physical access). And the LAN viewer is trustable-by-WiFi, which is a much weaker assumption on a coffee-shop network than at home — so we say "use it on your own Wi-Fi" out loud in the README, which is the camera-app equivalent of "this is the privacy boundary, drawn explicitly."
Curious what shape you land on for routing metadata at privacy.fish — selective batching to flatten timing signatures, or accept-and-document the leak? We treated it as accept-and-document (file-system timestamps are user-visible anyway), but mail is asymmetric (recipients matter), so the same answer probably doesn't fit.
For anyone landing here from a camera-app angle: play.google.com/store/apps/details...
Good question. For privacy.fish, I think the honest answer is mostly accept-and-document.
Mostly it de-risks expectations with technical users, and only partly with everyone else.
The useful effect is that it gives people a sentence to anchor on: “we reduce the provider-side data, but email still has routing metadata.” That stops some overclaiming early, especially with people who already know SMTP. For less technical users, “privacy-first mail” still tends to get heard as “private in every way,” so the boundary has to be repeated in plain-language places: onboarding, docs, support answers, and marketing.
That four-surface repetition — onboarding, docs, support, marketing — matches what I keep relearning on the camera-app side. "Privacy-first camera" gets heard as "no one can ever see anything," but the residual surface is real: the LAN-broadcast hostname when the local web server is on, app-list fingerprinting if anyone has device access, the SD card itself if the phone is physically taken.
The hardest place to repeat the boundary is the in-app settings screen. That's where users actually make the threat-model decision (turn LAN streaming on or off), and it's also where most apps default to silent toggles with no plain-language consequence text. We're trying a "what this exposes" line under each toggle now (in Background Camera RemoteStream — play.google.com/store/apps/details?id=com.superfunicular.digicam), which feels closer to the docs/onboarding repetition you described but lives at the point of decision instead of at the point of acquisition. Probably not enough on its own, but it stops the toggle-then-forget failure mode.
Ah nice, the two tables approach 🫡
You might be interested in our High-Performance Diffs, have you checked those out? docs.powersync.com/client-sdks/hig...
Thanks for the pointer, @kobie ! I really appreciate it.
Looks like this feature shipped right after we built our onChange-based pipeline, so we completely missed it. It seems very relevant to the two-table approach.
Will definitely try it out.
Our back-and-forth on Doszhan's E2E thread stuck with me — your 'accept-and-document' take, that structural SMTP leaks (recipient domain, source IP, social-graph at the mail-server layer) can't be wished away, so you'd rather draw the privacy boundary honestly than sell false batching mitigations. I just drafted a piece on the same tension one layer over: when AI agents dispatch real-world tasks to humans, 'proof the work happened' defaults to surveillance, and I think the honest move is minimum legible proof, owned by the worker — basically your framing applied to physical verification. Would love your read, and if it resonates I'd happily fold your angle in or co-sign: The Carbon Layer. Two privacy-first builders comparing notes.
play.google.com/store/apps/details?id=com.superfunicular.digicam