Privacy and data
What personal data the template stores, the self-service export and deletion flows, retention, and the checklist for adapting the legal pages.
The template is designed so the promises in the privacy policy are backed by real functionality. This page lists what is stored, how people exercise their rights, and what you must adapt before launch.
What is stored where#
| Data | Table | Lifecycle |
|---|---|---|
| Profile, verification, role | user |
Until the account is deleted |
| Password hash / OAuth links | account |
With the user |
| Sessions (IP, user agent) | session |
With the user; expired rows are removed by Better Auth |
| Contact messages | contact_message |
Archived ones are pruned; open ones too if a maximum age is set (see below) |
| Throttle counters (hashed IP/email) | throttle |
Expire after 15 minutes / 1 hour, pruned by db:prune |
| Audit log | audit_log |
Kept indefinitely; contains no message bodies or secrets |
Self-service#
From the dashboard a signed-in user can:
- Export their data:
src/pages/api/account/export.tsreturns profile, sign-in methods, sessions and, when the address is verified, contact messages from that address. Tokens and hashes are never included. - Delete their account through Better Auth’s
deleteUser. Password accounts confirm with the password; social-only accounts need a fresh session (younger than a day). ThebeforeDeletehook insrc/lib/auth.tsrefuses to remove the last administrator, andafterDeleteremoves contact messages from a verified address and records the deletion in the audit log. - Change their password, which signs out other sessions.
Requests from people without an account#
The contact form is the fallback. Messages arrive in the admin inbox; administrators can find everything tied to an address there and in the users page, act on it and archive the request. The audit log shows what was done and when.
Retention#
pnpm db:prune deletes archived contact messages older than CONTACT_RETENTION_DAYS (default 365) and expired throttle counters. Run it on a schedule:
on:
schedule:
- cron: '0 3 * * 0'
jobs:
prune:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup
- run: pnpm db:prune
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_AUTH_TOKEN: ${{ secrets.DATABASE_AUTH_TOKEN }}
By default open (new or read) messages are never deleted automatically, so
CONTACT_RETENTION_DAYS is not a maximum age for all contact data. If your policy needs one,
set CONTACT_MAX_AGE_DAYS as well: the same job then deletes every message older than that,
whatever its status, and says so in its output. Both values belong in the privacy policy.
The audit log records message status changes and deletions together with the change (one transaction); entries for everything else are best-effort. See the admin area before promising more than that in your policy.
Launch checklist#
- Edit
src/content/legal/privacy.mdandterms.md: legal entity, contact address, jurisdiction, and the providers you actually use (hosting, Turso, Resend, OAuth, analytics). - Set
CONTACT_RETENTION_DAYS(andCONTACT_MAX_AGE_DAYSif open messages must expire) to match the policy, and schedulepnpm db:prune. - Decide who the administrators are (
ADMIN_EMAILS) and make sure at least one exists. The application refuses to remove the last one. - Configure
RESEND_API_KEYso verification, resets and magic links work, or accept that those features stay hidden. - Document your hosting provider’s log retention in the policy.
- Update
public/.well-known/security.txtandSECURITY.mdwith real contacts.