CSV / Data Export Patterns: Chat Prompts
Customers want to export their data — to spreadsheets for analysis, to other tools for migration, to internal reports, or because they're considering churning and want their stuff out. "Export to CSV" is a feature you ship in week 1 and then quietly rebuild three times: first as a synchronous server response (works for 100 rows; times out at 10K), then as an async background job with email delivery (works for most cases; awkward for small exports), then as a robust system supporting filters, formats (CSV / Excel / JSON / PDF), large datasets via streaming, scheduled recurring exports, and access control.
This is the chat-prompt playbook for shipping data export that handles the small case fast (download in browser) and the large case correctly (background job, email, signed URL), without surprising users or your servers.
When This Belongs
Use a structured export system when:
- Users need to extract data for analysis, migration, or backup
- Datasets can grow large (1K+ rows is the threshold where naive sync export breaks)
- Multiple data types need export (transactions, customers, events, etc.)
- Compliance or contractual obligation (GDPR data export, etc.)
Don't bother when:
- Pre-revenue product without users
- Single small dataset where a static download works
Three Export Patterns by Size
The most important architectural decision: pick the right pattern for the data size.
Pattern 1: Synchronous Browser Download (small datasets)
For exports under ~5K rows or ~5MB:
I want to add CSV export for my [resource, e.g. "filtered transaction list"]. The user clicks "Export"; CSV downloads immediately.
Implementation:
1. User clicks "Export CSV" button
2. Backend queries data with current filters
3. Stream rows as CSV directly to the response
4. Set headers: Content-Type: text/csv; Content-Disposition: attachment; filename="export-[date].csv"
5. Browser saves to Downloads folder
Use streaming (don't buffer the whole CSV in memory) — Node's `Readable` stream + `csv-stringify`.
Limit: cap at 10K rows; if user requests more, suggest async background export.
Show me the implementation as a Next.js route handler.
Stack: Next.js + csv-stringify or papaparse.
Pattern 2: Async Background Job + Email (medium-large datasets)
For exports between ~5K and ~1M rows:
For larger exports, switch to async:
1. User clicks "Export"; modal: "Generating export. We'll email you when ready (usually 1-2 minutes)."
2. Backend creates an `export_job` record with status='pending'
3. Background worker (Inngest / Trigger.dev / Vercel Queues) processes:
- Stream query results
- Write CSV to object storage (Vercel Blob / S3)
- Generate signed URL (24-hour expiry)
- Mark job complete
4. Email user with download link
5. User clicks link → downloads from object storage directly (not your server)
Edge cases:
- Job fails: email user with error + retry option
- Signed URL expires: user can request re-generation
- User cancels mid-job: clean up partial file
Stack: Next.js + Inngest + Vercel Blob + Resend.
Pattern 3: Streaming Direct Download (very large datasets)
For datasets >1M rows where buffering whole file is impractical:
For very large exports, use streaming download with Server-Sent Events or chunked transfer:
Option A: Stream CSV via HTTP chunked transfer
- Same as sync pattern but for very long-running response
- Risk: connection drops; user has to restart
- Works only for fast-enough queries
Option B (recommended): Async + multi-part upload
- Background job streams to S3 multi-part upload
- Mark complete when full file is uploaded
- Email user the link
Option C: Database-direct export (Postgres COPY)
- For Postgres: use COPY command to dump query results directly to file
- Fastest option for large datasets
- Bypasses application layer
Pick option B for most cases. Show me the implementation.
Stack: Next.js + Inngest + Vercel Blob + Postgres COPY (optional).
The Job Tracking System
Build the export-job tracking system:
Schema:
```sql
export_jobs:
id, user_id, account_id, resource_type (e.g. 'transactions', 'customers'),
filters (JSON), format (csv | xlsx | json | pdf),
status (pending | running | succeeded | failed | cancelled),
rows_exported, file_size_bytes,
storage_url (nullable), signed_url_expires_at,
error_message,
created_at, started_at, completed_at
UI:
- "/exports" page: list past + in-progress export jobs for the user
- Per-job: status, format, rows, size, download link (if succeeded), retry button (if failed)
- New export form: pick resource + filters + format
Cleanup:
- Delete export files older than 30 days from storage (cron job)
- Keep job records for 1 year for audit
- User can cancel pending / running jobs
Stack: Next.js + Drizzle + Vercel Blob + Inngest.
## Format Support: CSV vs Excel vs JSON vs PDF
Different users want different formats. Implement multi-format export:
CSV (most common):
- Use
csv-stringify(Node) - Quote fields containing commas / newlines
- UTF-8 BOM at start (Excel needs it for non-ASCII characters)
Excel (.xlsx):
- Use
exceljslibrary - Preserve column types (dates, numbers, booleans)
- Add column header styling (bold, frozen first row)
- Multiple sheets if data has natural divisions
JSON:
- Stream JSON array
- Pretty-print optional
- Preserve nested data structure (CSV flattens)
PDF:
- For reporting / shareable formats
- Use
puppeteerorpdfkitto render an HTML table - Worst format for large exports — limit to <1K rows
Implement a switch based on the format parameter; share the underlying query.
Stack: Next.js + csv-stringify + exceljs + pdfkit.
CSV is the default; offer Excel as a paid-tier upgrade for some products. JSON is for technical users; PDF for executives wanting reports.
## Filtering and Scoping
Exports should respect the user's current view filters.
UI pattern:
- User filters a list (e.g., "Transactions > $100 in October")
- Click "Export" — automatically applies the same filters
- Confirmation: "Export 1,247 rows matching your filters? [Yes / Cancel]"
Implementation:
- The export endpoint accepts the same filter params as the list endpoint
- Re-run the query with those filters
- Surface estimated row count in the confirmation
Edge: user changes filters between confirmation and click — re-run count.
Stack: Next.js + Drizzle.
## Column Selection
Users want to choose which columns to export.
UI:
- Export modal includes "Columns" multi-select
- Default: all columns
- User unchecks any they don't want
- Save column-set as a "view" for repeat exports
Implementation:
- Whitelist columns server-side (don't trust client to control DB queries)
- Map column names to safe SQL projection
- Validate against user's permissions per column
Stack: Next.js + Drizzle + zod for validation.
## Scheduled / Recurring Exports
Some users want weekly / monthly automatic exports — emailed on schedule.
Schema:
scheduled_exports: id, user_id, resource, filters, format, schedule (cron expression), recipients (email array), last_run_at, next_run_at, status
Implementation:
- User configures: "Email me a CSV of new customers every Monday at 9am"
- Cron job (every 15 min) finds scheduled exports due to run
- Each due export creates a regular export_job
- On completion, email recipients with download link
UI:
- "/exports/scheduled" page to manage recurring exports
- Edit, pause, delete
Stack: Next.js + Vercel Cron + Inngest + Drizzle.
## GDPR Data Subject Access Request (DSAR)
For EU users, you must provide all their personal data on request.
Build the GDPR data export flow:
When user clicks "Download my data" in account settings:
- Validate request (the user's authenticated account)
- Create export job with all PII-bearing data:
- Profile (name, email, phone)
- Account settings + preferences
- Transactions / invoices
- Activity / events
- Files / uploads
- Communications (support tickets, in-app messages)
- Format as a JSON archive (preferred) or ZIP of CSVs
- Email user with secure download link
- Log the request for audit (compliance evidence)
Constraints:
- Must complete within 30 days (GDPR mandate)
- Must include ALL data we hold on the user
- Must be portable (machine-readable format)
Implement:
- The data-collection function spanning all user-touching tables
- The packaging into a single archive
- Secure delivery (signed URL; expiring; access-logged)
Stack: Next.js + Drizzle + Vercel Blob + Inngest.
## Access Control on Exports
Exports respect user permissions:
- A team member can only export data they have permission to read
- An admin can export their entire workspace
- A read-only viewer cannot export at all (or only their own data)
Implementation:
- Apply the same authorization layer to exports as to the list view
- Don't trust client to filter; do it server-side
- Log every export request: who, when, what filters, how many rows
- Audit-log entry feeds into compliance reporting
For sensitive data (financial, health, PII):
- Require recent re-authentication (sudo mode)
- 2FA confirmation
- Notify account admins when exports happen
- Watermark or include export-tracker metadata
Stack: Next.js + your existing auth + audit logs.
## Common Pitfalls
**Synchronous export of 100K rows.** Times out; user gets a 504; tries again; angry. Always async past ~5K rows.
**Buffering whole CSV in memory.** OOMs on the server for large datasets. Always stream.
**No status feedback during async export.** User waits 5 minutes wondering if it's working. Email + status page show progress.
**Signed URL with no expiration.** Customer's email forwarded; data leaks. Always 24h expiry; regenerate on request.
**Storing exports forever.** Storage costs balloon. Delete after 30 days; user can re-request.
**No filter / column selection.** User exports 50 columns; only needs 5. Frustrating. Allow column selection.
**No format options.** CSV-only; Excel users cry. Offer Excel + CSV at minimum.
**UTF-8 issues.** Excel opens UTF-8 CSV showing garbage characters. Add UTF-8 BOM (``) at start.
**Column types lost in CSV.** Numbers become strings; dates become strings. CSV's limitation. Excel preserves types.
**No way to retry failed exports.** User waits; gets failure email; has no recourse. Build retry button.
**Permissions ignored on export.** A read-only user exports everything. Same auth as list endpoint.
**No audit log of exports.** Customer's data exported by an attacker; nobody knows. Log every export.
**Exports counted in product analytics events.** Pollutes your usage data with system-generated traffic. Tag exports separately.
**No row-count preview.** User exports thinking it's 100 rows; it's 100K. Add count preview before generating.
**Cancellation not supported.** User clicks "Export 1M rows"; realizes mistake; can't cancel. Allow cancellation of pending / running jobs.
**No scheduled-export option.** Power users want recurring; have to manually trigger weekly. Add scheduled exports.
**Filename collisions.** Same filename for different exports; older ones overwritten. Include timestamp + UUID.
**Exporting joined data wrong.** Customer table joined to addresses; one customer with 2 addresses produces 2 rows in CSV. Decide: separate sheets in Excel? Comma-joined in single CSV column? User chooses.
**No GDPR-compliant data export.** EU users request data; you have nothing built. GDPR violation. Build the DSAR flow.
**Exports of sensitive data without 2FA.** Financial / PII export with no re-authentication. Add sudo mode for sensitive exports.
**Heavy exports running on app servers.** Single big export crashes the app. Run in background workers; isolate.
## See Also
- [Account Deletion & Data Export](./account-deletion-data-export-chat.md) — full account-level export
- [GDPR / Privacy](./cookie-consent-chat.md) — adjacent compliance
- [Audit Logs](./audit-logs-chat.md) — log every export
- [Background Jobs & Queue Management](./background-jobs-queue-management-chat.md)
- [Cron / Scheduled Tasks](./cron-scheduled-tasks-chat.md)
- [Email Template Implementation](./email-template-implementation-chat.md) — export-ready emails
- [Email Deliverability](./email-deliverability-chat.md)
- [Customer Reports & Scheduled Exports](./customer-reports-scheduled-exports-chat.md) — overlapping; this article focuses on CSV/data export specifically
- [Roles & Permissions](./roles-permissions-chat.md)
- [Audit Logs](./audit-logs-chat.md)
- [Multi-Tenancy](./multi-tenancy-chat.md)
- [API Pagination Patterns](./api-pagination-patterns-chat.md)
- [PDF Generation In-App](./pdf-generation-in-app-chat.md)
- [Internal Admin Tools](./internal-admin-tools-chat.md)
- [Approval Workflows & Multi-Step Routing](./approval-workflows-multi-step-routing-chat.md)
- [Settings & Account Pages](./settings-account-pages-chat.md)
- [Sandbox & Test Mode for SaaS APIs](./sandbox-test-mode-saas-apis-chat.md)
- [Toast Notifications UI](./toast-notifications-ui-chat.md)
- [In-App Status Banners & System Notifications](./in-app-status-banners-system-notifications-chat.md)
- [Microcopy & Product Copy Systems](./microcopy-product-copy-systems-chat.md)
- [File Storage Providers (VibeReference)](https://viberef.dev/cloud-and-hosting/file-storage-providers.md)
- [Vercel Blob (VibeReference)](https://viberef.dev/cloud-and-hosting/vercel-blob.md)
- [Email Providers (VibeReference)](https://viberef.dev/backend-and-data/email-providers.md)
- [Resend (VibeReference)](https://viberef.dev/backend-and-data/resend.md)
- [Background Jobs Providers (VibeReference)](https://viberef.dev/backend-and-data/background-jobs-providers.md)