Compliance is a first-class concern for a SaaS boilerplate. This page covers the built-in privacy mechanics for GDPR-style data-subject rights plus the launch checks needed for US, Canadian, and Swiss markets. This is an implementation baseline, not legal advice; review the final policy text and processor list with counsel before production launch.
| Right | How it's served |
|---|---|
| Consent | Cookie banner with necessary/analytics/marketing categories; anonymous choices persist locally, while logged-in choices sync via POST /api/user/consents. Third-party analytics/chat scripts mount only after matching consent. |
| Withdrawal | The footer exposes Cookie settings and Your Privacy Choices so users can reopen preferences, withdraw analytics/marketing consent, and reach privacy-rights actions after the banner is dismissed. |
| Access / portability (Art. 15/20) | GET /api/user/export-data streams a multi-section RFC 4180 CSV covering profile, memberships, chat/AI, consents, payments, subscriptions, licenses, invitations, access/admin logs tied to the user, deletion requests, notifications, pending emails, document metadata, and API-key metadata without secrets/hashes. It is scoped to the caller's own data only. |
| Erasure (Art. 17) | Self-service deletion request enters a 30-day grace queue (account_deletion_requests). The process-account-deletions job cancels Stripe subscriptions, removes the newsletter contact, cascades licenses to chat to payments to subscriptions to memberships to account, deletes Storage objects (not just rows), then the auth.users row. |
| B2B owner cascade | account_deletion_requests.cascade_members distinguishes "delete my personal account in B2B mode" (cascades workspace members' auth rows) from workspace-only deletion (members preserved). Set by the requestAccountDeletion server action; never elsewhere. |
| US state privacy | The footer links to /[locale]/privacy-choices, a dedicated "Your Privacy Choices" page for do-not-sell/share and targeted-advertising opt-out visibility. Cookie consent also honors browser Global Privacy Control signals by keeping marketing/sharing/targeted-ad consent disabled even when "Accept all" is clicked. |
| Canada | The default policy covers meaningful consent, reasonable purposes, safeguarding, consent withdrawal, breach-response procedures, and CASL-style commercial-message opt-in/unsubscribe expectations. |
| Switzerland | The default policy covers Swiss FADP/nFADP-style transparency, proportionality, purpose limitation, security, data-subject rights, international-transfer safeguards, and FDPIC high-risk breach notification expectations. |
| Marketing email | POST /api/newsletter/subscribe is Turnstile-protected and records server-side consent provenance attributes (CONSENT_SOURCE, CONSENT_TEXT, CONSENT_AT) with the configured email provider. |
Deletion requests are proof-bearing: processor_status, processor_errors, error_message, and retry_count preserve redacted outcomes for Stripe, newsletter/email, Storage, database cascade, and Supabase Auth. A failed processor marks the request failed instead of falsely completing it.
Customer-facing routes: /[locale]/privacy-choices, /[locale]/privacy, /[locale]/terms, and /[locale]/legal. API routes: GET /api/user/export-data, POST /api/org/schedule-deletion, POST /api/org/cancel-deletion (all authenticated; deletion is owner-only at the API). Before launch, edit the terms / privacy / legal CMS pages and confirm your processor list (Stripe, Supabase, email provider, GTM, AI providers).
US/Canada/Switzerland launch checklist: publish real business contact details; verify commercial email templates identify the sender and include unsubscribe where the message is promotional; confirm whether your analytics/ads setup is sale, sharing, or targeted advertising and that /privacy-choices accurately describes it; document breach notification ownership; and create Stripe CAD/CHF prices if Canadian or Swiss checkout is enabled.
Also set LEGAL_COMPANY_NAME, LEGAL_ADDRESS, and LEGAL_REGISTRATION_NUMBER to real production values before enabling NEXT_PUBLIC_INDEXABLE=true. Example placeholders intentionally block public/indexable production builds.