Skip to content
Strata

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.ts returns 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). The beforeDelete hook in src/lib/auth.ts refuses to remove the last administrator, and afterDelete removes 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#

  1. Edit src/content/legal/privacy.md and terms.md: legal entity, contact address, jurisdiction, and the providers you actually use (hosting, Turso, Resend, OAuth, analytics).
  2. Set CONTACT_RETENTION_DAYS (and CONTACT_MAX_AGE_DAYS if open messages must expire) to match the policy, and schedule pnpm db:prune.
  3. Decide who the administrators are (ADMIN_EMAILS) and make sure at least one exists. The application refuses to remove the last one.
  4. Configure RESEND_API_KEY so verification, resets and magic links work, or accept that those features stay hidden.
  5. Document your hosting provider’s log retention in the policy.
  6. Update public/.well-known/security.txt and SECURITY.md with real contacts.