Munchable Privacy: Account Export and Data Deletion

ملخص

Pixel shares a code review of Munchable’s account export and deletion routes, highlighting a mismatch in their database coverage. The export gathers data from five tables, while the announcement notes that conditions, sensitivities, and scan history stay on the device and are not sent to servers. These details matter for making privacy promises verifiable.

DEV Community

Our delete route touches seven tables, our export route reads five, and the two it misses hold the only free text we have

Munchable is a barcode scanner for people with gut conditions, and the thing that makes its privacy notice short is that the health data never arrives: the conditions you pick, your sensitivities and your scan history are on the phone, and the verdict is worked out there too. The policy is at munchable.app/privacy, and the sentence it leads with is that we hold an email address, a subscription record and any labels you contributed. Two route handlers have to make that sentence checkable: one that hands you everything we hold, and one that erases it. They live in sibling folders, they were written months apart, and they disagree about the schema. The export, in full GET /api/export-account is five queries in parallel and one JSON response. const [ent, consent, rewards, contributions, feedback] = await Promise.all([
db.select().from(entitlements).where(eq(entitlements.userId, uid)).limit(1),
db.select().from(userConsent).where(eq(userConsent.userId, uid)).limit(1),
db.select().from(contributionRewards).where(eq(contributionRewards.userId, uid)),
db.select({ barcode: ..., ingredientsText: ..., createdAt: ... })
.from(catalogProductRevisions)
.where(eq(catalogProductRevisions.contributorId, uid)),
db.select().from(catalogProductFeedback).where(eq(catalogProductFeedback.userId, uid)),
]);
It answers 401 to a signed-out caller and to an anonymous session, serialises with JSON.stringify(payload, null, 2) because a human is going to open it, and sets a content-disposition so it lands as a file rather than a wall of text. The field I want to point at is not a table. It is this: note: 'Your conditions, sensitivities, and scan history are stored only on your device and are never sent to our servers, so they are not part of this export.',
An export of almost nothing looks broken. Someone who scans twenty products a week opens this file, sees a subscription row and two contributions, and reasonably concludes the export is incomplete. So part of the payload's job is to explain its own smallness, in the file, where someone reading the file will see it. Our privacy notice says the same thing, but a notice is not what lands in the Downloads folder. The erasure, and the list it uses POST /api/delete-account is a different shape: cancel the Stripe subscription, then one transaction, then delete the identity. await tx.delete(contributionRewards).where(...);
await tx.delete(catalogProductFeedback).where(...);
await tx.update(catalogProductRevisions).set({ contributorId: DELETED_CONTRIBUTOR }).where(...);
await tx.delete(userConsent).where(...);
await tx.delete(entitlements).where(...);
await tx.delete(supportMessages).where(inArray(supportMessages.ticketId, / subquery /));
await tx.delete(supportTickets).where(...);
Seven tables. Six deletes and one update: contributed revisions are re-pointed at a fixed sentinel id rather than removed, because the catalogue rows that reference them are not personal data and should not vanish from other people's app when one contributor leaves. The route also refuses to start if it cannot finish. Without the Supabase service-role key it cannot delete the identity itself, so it answers 501 rather than deleting the rows and leaving the login, which would be a half-erasure that returns 200. The diff between the two lists Side by side: table erased exported
entitlements yes yes
user_consent yes yes
contribution_rewards yes yes
catalog.product_revisions anon 3 of 18 columns
catalog.product_feedback yes yes
support_tickets yes NO
support_messages yes NO
The two tables the export does not read are the two the erasure route is most emphatic about. Its own comment: // Support history goes with the account. A ticket body is whatever the
// person typed, so it is the most sensitive free text Munchable holds and
// erasure has to take it.
One route calls it the most sensitive free tex...
Munchable Privacy: Account Export and Data Deletion
Powered by Readybot.io

الأحدث من Pixel

Fix GitHub Pages: Missing Artifact Error

Pixel's announcement explains why the GitHub Pages deploy action reports that no github-pages artifact was found. It covers skipped or failed build jobs, missing uploads, …

Rotate DB and Redis Passwords Without Downtime

Pixel shares a practical guide to rotating exposed database and Redis passwords without interrupting application connections. The approach updates Secrets, changes service credentials, and gradually …

AI Agent Earns Crypto from $0: Day 3 Update

Pixel shares Day 3 of a 21-day experiment testing whether an autonomous AI agent can earn real crypto from zero. The agent launched a pay-per-call …

AI Agent Receipts Prove What Actually Executed

Pixel’s announcement examines how public Zambo receipts verify what an AI agent actually did. A BTC price fetch succeeded with a recomputable output hash, while …