Account & Data Export
Account & Data Export
Warpkit gives users full control over their data: GDPR-compliant export and permanent account deletion, both built in.
Data export
exportMyDataFn returns a JSON snapshot of everything the user owns:
import { exportMyDataFn } from '@/features/account';
const result = await exportMyDataFn();
// result.data = {
// exported_at: '2026-05-18T00:00:00.000Z',
// profile: { name, email, createdAt },
// subscription: { plan, status, currentPeriodEnd } | null,
// data: {
// notes: [...],
// user_events: [...],
// notifications: [...],
// api_keys: [...],
// files: [...],
// settings: [...],
// conversations: [...],
// chat_messages: [...],
// }
// }
dumpUserDbTables (src/features/account/server/account.server.ts) builds the data object from an explicit, hand-maintained list of every user-scoped table , notes, userEvents, notifications, apiKeys, files, settings, conversations, chatMessages, each queried with .where(eq(table.userId, userId)). There is no Postgres equivalent to "list this user's tables" the way sqlite_master let the old per-user-SQLite version introspect dynamically , shared tables filtered by user_id have no such catalog to walk. Adding a new user-scoped table does not automatically appear in the export: you must add it to dumpUserDbTables's hand-maintained list in the same change, or its rows are silently missing from every user's export and from the account-deletion confirmation email.
Redaction is inline, not a separate allowlist. The old REDACTED_COLUMNS constant doesn't exist anymore , dumpUserDbTables redacts secret-bearing columns directly in the query result, e.g. apiKeysRows.map(row => ({ ...row, keyHash: '[redacted]' })). If you add a new user-scoped table with a secret/token/credential column, add the same inline redaction when you add the table to the hand-maintained list , otherwise that column ships verbatim in the user's downloaded JSON. settings.value is intentionally left unredacted: users need their own settings, including any encrypted values, in their own export.
What this means for GDPR: Article 20 (data portability) is satisfied out of the box. Users can download everything in one click , but treat inline redaction as a required checklist item whenever a new table can hold secret material.
Account deletion
deleteMyAccountFn triggers a durable, resumable teardown (src/lib/operations/account-deletion.server.ts) with these steps, in order:
- Cancel Stripe subscription (if active)
- Delete Stripe customer record
- Delete S3-hosted uploads (
listFiles(db, userId), thenDeleteObjectCommandper file) , must run before step 5, since thefilestable's rows (and thes3_keyneeded to clean up the bucket) cascade-delete along with the user row - No-op marker , there is no Postgres equivalent to "delete the per-user SQLite file". Every user-scoped table (
notes,userEvents,notifications,apiKeys,files,settings,conversations,chatMessages) has anonDelete: 'cascade'FK tousers.id, so cleanup happens automatically when step 5 deletes the shared user row. This step is kept as a no-op marker, not removed, so the job's step timestamps stay a faithful record of what ran - Delete the user row from the shared DB (POINT OF NO RETURN , cascades away every remaining user-scoped table row via the FK constraints from step 4)
import { deleteMyAccountFn } from '@/features/account';
const result = await deleteMyAccountFn();
Stripe cancellation and S3 cleanup are non-fatal per-step: a transient failure persists an error on the job row and stops that attempt, but doesn't corrupt state , the job resumes from the same step on the next run (self-hosted's reconciler, or a retry). The user row deletion at step 5 is the authoritative cleanup and the point after which the operation can't be rolled back.
Where it lives
src/features/account/server/account.queries.ts # exportMyDataFn
src/features/account/server/account.server.ts # dumpUserDbTables (export table list + redaction)
src/features/account/server/account.mutations.ts # deleteMyAccountFn
src/lib/operations/account-deletion.server.ts # durable deletion state machine
Deletion is implemented as a durable state machine. It persists each step to the shared account_deletion_jobs table so a server crash mid-deletion resumes on the next reconcile pass rather than leaving the user in a broken state , a lease (leaseExpiresAt) prevents two concurrent runs from racing the same job. Already-completed steps (tracked by individual *At timestamp columns) are skipped on retry.
Both exportMyDataFn and deleteMyAccountFn require an authenticated session. Unauthenticated calls return UNAUTHORIZED.