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
issandaudclaims. - 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_aton 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_logsand 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_idat 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).