Security

What we actually do today, written by the engineer who built it — no aspirational badges.

Encryption

  • TLS 1.2+ on every connection. Caddy in front of the API terminates TLS with auto-renewed certificates.
  • OAuth refresh tokens (Gmail / Outlook), TOTP secrets, and other sensitive fields are individually encrypted with AES-256-GCM. Each ciphertext is bound to the row it belongs to via additional-authenticated-data (AAD), so a tampered row swap fails the auth-tag check.
  • The mobile app's on-device SQLite database is encrypted with SQLCipher (AES-256). Tokens land in the platform secure keychain (iOS Keychain, Android Keystore) with hardware-backed encryption when the device supports it.
  • Passwords are bcrypt-hashed with cost 12, pre-hashed with SHA-256 so passwords above 72 bytes don't lose entropy.

Authentication

  • JWT access tokens (1-hour TTL) + opaque refresh tokens (7-day TTL, rotated on every use). Tokens are bound to a specific environment via iss and aud claims.
  • Refresh-token reuse triggers immediate revocation of every active session for that user — a leaked-and-replayed refresh token kills its family within one request.
  • Optional TOTP MFA with single-use backup codes. MFA secrets are stored encrypted (see Encryption above).
  • Suspending an account or changing the password stamps tokens_invalidated_at on the user row; any access token issued before that moment is rejected by middleware on the next request.
  • OAuth sign-in via Google and Apple. Apple's nonce claim is verified against the client-supplied value to defang token replay.

Audit logging

  • Every sensitive action (sign-in, password change, admin operation, money-moving commission state change) writes a row to audit_logs.
  • The table is hash-chained: each row stores SHA-256 of (previous-hash || row payload). A daily verifier walks the chain; any break sends a critical alert.
  • The chain trigger uses a UTC-canonical timestamp so writer and verifier produce the same hash regardless of session timezone.
  • The API role does not own audit_logs and cannot drop the immutable-row trigger or DELETE rows. The retention cleanup runs as a separate Postgres role.

Application protections

  • Every database query that touches user-owned data is scoped by user_id at the application layer. The schema's foreign keys mirror this so a missing scope clause fails closed.
  • Mutating endpoints support a client-supplied Idempotency-Key; the partner-gift creation endpoint requires it, so a double-tap can never produce two charges.
  • Per-IP and per-account rate limits on sign-in, refresh, password reset, and other sensitive endpoints. Limits are enforced through Redis so a multi-replica deploy doesn't multiply effective budgets.
  • CSRF protection on the partner dashboard via double-submit cookie. The Next.js proxy strips cookies before forwarding to the API and rejects cross-origin mutations.
  • Stripe webhooks verify the raw-body signature before any handler runs. Replays of out-of-order events are gated by a per-charge high-water mark.

Account deletion

  • Deletion is soft, with a 30-day cooling-off window. Sign in during the window to recover; after 30 days, a cron permanently erases the account.
  • The purge harvests every object the user owns from object storage (receipt photos, documents, avatars) before the SQL cascade fires.
  • OAuth grants (Gmail / Outlook) are revoked at the provider, not just locally, on disconnect or account deletion.
  • Push-notification tokens are deleted on sign-out and at account deletion.
  • The hard-delete event itself is recorded in the audit chain (with the user's email preserved in a denormalized column) so we can later answer "did we delete this user, and when?".

Hosting

  • We run on DigitalOcean droplets we manage directly. Postgres, MinIO (S3-compatible object storage), and Redis live on the same private network behind Caddy.
  • We don't yet operate from multiple regions, do point-in-time database recovery, or run formal disaster-recovery drills. As we scale we'll add these; we'd rather not claim them prematurely.
  • We don't hold SOC 2, ISO 27001, or any third-party security certification. We're a small team building toward those as the user base grows.

Reporting a vulnerability

Email security@havenkeep.app with details of any security issue you find. We don't run a paid bug-bounty program yet, but we'll respond personally within a few business days and credit responsible disclosures publicly (with your permission).

Questions?

We're happy to walk through anything specific.

Contact us