Security
Every claim on this page is backed by a continuously-running test.
The companion to /privacy. Privacy answers "what do you do with my data?"; this page answers "how do I know you actually do that?"
For the line-by-line collection-level retention list see the engineering source-of-truth at /app/docs/DATA_RETENTION.md in our repo (linked from each release).
Security posture
The defenses in place today. Each control is verified by an automated test that runs on every commit.
Transport security
- ·HTTPS-only — TLS 1.3 enforced at Cloudflare edge.
- ·HSTS preloaded with includeSubDomains; max-age 1 year.
- ·Strict CSP with nonce-per-request on every HTML response.
- ·HttpOnly, Secure auth cookies use SameSite=None for supported app domains, with Origin-checked mutation routes.
Authentication
- ·Bcrypt password hashing, 12 rounds (~250ms target — slows offline attackers).
- ·Optional TOTP second factor (RFC 6238, 30-second windows).
- ·Optional WebAuthn / passkey enrollment with platform-bound credentials.
- ·Brute-force lockout after 5 failed attempts (15-minute window, IP+email scoped).
- ·Step-up auth required for sensitive admin actions.
Data at rest
- ·MongoDB with WiredTiger storage engine + AES-256 disk encryption.
- ·Document-level field encryption for credential material (TOTP secrets, passkey IDs).
- ·Backups encrypted with separate keys, retained 30 days hot + 1 year cold (deploy/SRE owns).
Application hardening
- ·Rate-limited at 4 layers (Cloudflare, Nginx, FastAPI middleware, per-route).
- ·Pydantic-validated request bodies (no raw deserialisation paths).
- ·OWASP top-10 mitigations covered by validation tests in tests/validation/security_audit.
- ·Server-Action WAF (Session 138) blocks reflected ID-spray attempts on Next.js mutations.
Privacy by design
- ·No third-party tracking pixels (verifiable in any devtools Network tab).
- ·Marketing-funnel analytics use a client-generated visitor_id (UUID), never a user_id.
- ·Public viability share links scrub all owner identifiers and use anonymised view-count hashes.
- ·Outbound emails carry no tracking pixels.
OCR / document handling (Pro)
- ·Statements are preprocessed by Viably and sent to the configured Gemini OCR model for extraction.
- ·Documents are scoped to the uploading user — DB-layer ACL enforced on every query.
- ·Quota-enforced 5 free scans / month; Pro lifts the cap. 402 returned on quota exhaustion (no silent failures).
- ·Documents are TTL-eligible: delete in-app and the row is hard-deleted, OCR-extracted text included.
Data retention windows
Every row below is enforced by a MongoDB TTL index registered in backend/services/perf_indexes.py and asserted by a validation test that fails the build if the index is missing.
| Data | Window |
|---|---|
| Audit logs (security events) | 365 days |
| In-app notifications | 90 days (30 post-dismiss) |
| Marketing-funnel analytics | 90 days |
| Active session records | 90 days inactive |
| Insight snapshots (cache) | 180 days |
| JWT session rows | 24 hours / 30 days |
| Auth artifacts (OTP, password reset, MFA challenges) | 5 minutes – 1 hour |
| Brute-force lockout state | 1 hour |
| Idempotency keys | 1 day |
Account deletion contract
One click, one cascade, no footgun.
When you delete your account from Settings → Security, the API runs a transactional cascade that hard-deletes every row in:
- users · workplaces · pay_rules · shifts · shift_templates
- bills · debts · goals · budget_categories · time_off_*
- households · household_invites · household_goal_contributions
- documents · document_extractions · ocr_corrections · receipts
- viability_scenarios · viability_shares · viability_share_views · viability_exports
- auth_sessions · user_sessions · active_sessions · mfa_* · webauthn_credentials
- notifications · merchant_aliases · user_notification_preferences
- decisions · feedback_reports
Preserved: audit_logs entries (anonymised, age out via 365-day TTL), payment_transactions (financial-ledger retention; mirrored by Stripe). Both are retention-bounded.
Commitments + honest disclaimers
What we'll commit to. What we won't pretend to be yet.
Verified by tests
- ·Every TTL above has a validation test in backend/tests/validation/test_iter162_notifications_ttl.py that fails CI if the index is missing or the field type is wrong.
- ·Auth + brute-force flows have integration coverage; the security gate (tests/validation/test_eslint_security_gate.py) blocks merges on disabled security rules.
- ·Account-deletion cascade has end-to-end coverage; an orphan-row scan runs in CI for every collection touched by the cascade.
Honest disclaimers
- ·We are not yet SOC 2 Type II certified. We design for the controls but the audit is on the roadmap, not complete.
- ·We are not a HIPAA business associate. Don't upload Protected Health Information; use a HIPAA-compliant tool for that.
- ·We do not maintain a public bug-bounty program yet. Responsible-disclosure reports go to security@viably.app and we reply within 2 business days.
How drift is caught
- ·/app/docs/DATA_RETENTION.md is the canonical source. The validation tests check that every documented TTL is actually registered as a MongoDB index.
- ·If an engineer adds a new collection without TTL or with the wrong field type, CI fails and the PR can't merge.
- ·Materially-changed retention windows are released in the public changelog with at least 30 days notice for tightening windows.
Reporting a security issue
Email security@viably.app. PGP key on request. We aim to acknowledge within 1 business day, triage within 3, and ship a fix or mitigation within 10 for high-severity findings.
We do not yet operate a paid bug-bounty program but we will publicly credit responsible-disclosure researchers in our changelog at your discretion.
Questions answered
The security posture is real.
The product is too.
Run your audit Free. No credit card. Account creation requires only an email.