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:

  1. Cancel Stripe subscription (if active)
  2. Delete Stripe customer record
  3. Delete S3-hosted uploads (listFiles(db, userId), then DeleteObjectCommand per file) , must run before step 5, since the files table's rows (and the s3_key needed to clean up the bucket) cascade-delete along with the user row
  4. 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 an onDelete: 'cascade' FK to users.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
  5. 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.