Infrastructure
Security
Last updated: August 24, 2026
Security posture
Finance OS is a private, single-operator financial system. It has no customers and processes no third party's financial data. This page is an honest assessment of what is actually implemented — not a marketing page. Everything listed under “What is not in place” is a real gap.
The controls below are documented in full, with pointers to the code and migrations that implement them, in the written Information Security Policy (docs/INFORMATION_SECURITY_POLICY.md), maintained in the Finance OS repository and available on request.
Security governance
- Named security owner — Ryan DeCook is the Information Security Officer, and as sole operator holds every role: system owner, administrator, developer, and incident responder.
- Written information security policy — Version 1.8, effective August 20, 2026 and revised August 24, 2026. Covers scope, data classification, access control, encryption, secrets handling, vendors, logging and retention, incident response, vulnerability scanning, and secure development. Reviewed at least annually and on material control changes.
- Documented incident response procedure — detect, contain, assess, notify, remediate and record.
- No separation of duties — with a single operator there is no second approver and no on-call rotation. This is an accepted, documented risk.
Access control and authentication
- Supabase Auth — email and password sign-in. Passwords are hashed by Supabase; Finance OS never sees, stores, or logs a password. Minimum password length is 12 characters.
- Multi-factor authentication (TOTP) — required, not optional. After password sign-in, the session must complete a TOTP challenge from an authenticator app before any application page or API route responds. An account with no enrolled factor is forced through enrollment before it can use the app at all. TOTP enrollment and verification are handled by Supabase Auth; the secret lives in the operator's authenticator app and is never stored or logged by Finance OS.
- WebAuthn passkeys — implemented in the application, not yet operational. The login page, the MFA gate, and the settings screen all support passkeys: passkey sign-in as a first factor, a passkey as the second factor, and passkey enrollment and removal. The Supabase project is configured for it (relying party
finance.fyi, originshttps://finance.fyiandhttps://www.finance.fyi). It does not work today: the project's auth server does not serve the WebAuthn endpoints, and Supabase currently refuses to enable WebAuthn as an MFA factor at all. The interface detects this and hides the passkey controls rather than offering a button that fails, so the second factor actually in use is TOTP and this page does not claim phishing-resistant MFA. The controls appear on their own once Supabase enables it. - Session length — a completed sign-in is remembered for 30 days, after which a full re-authentication (first factor plus second factor) is required. This is enforced in two places that must agree: the session cookie's lifetime, and a 30-day server-side session timebox on the Supabase project, which is what actually revokes the refresh token. The access token itself expires every hour and is rotated by the refresh token, so 30 days is how long the operator goes without signing in again — not how long a stolen access token stays valid.
- Server-side route protection — middleware validates every request to
/app/*against the Supabase Auth server (not just the cookie's contents), redirects unauthenticated requests to the login page, and redirects sessions that have not completed MFA (assurance level below AAL2) to the MFA gate. API routes apply the same AAL2 check. - Row-level security — Postgres RLS is the row-visibility boundary for user-session access, keyed on entity ownership or the actor's user id.
- Private document storage — the documents bucket is private with owner-only read, write, and update policies. There is deliberately no end-user delete policy, so uploaded evidence cannot be destroyed from a browser session.
- Authenticated API routes — every API route requires a valid session before doing any work.
- Plaid consent and deletion — Link requires an AAL2-authenticated, versioned consent receipt bound to one Finance OS account. Disconnect revokes the Plaid Item and consent, removes encrypted tokens and raw provider data, and deletes unreviewed staged transactions. Reviewed books and audit evidence follow the documented retention policy.
- Central application identity — Supabase Auth is the single identity, session, and MFA service for Finance OS. Vendor administrative identities are still vendor-native, so Finance OS does not claim organization-wide workforce IAM.
- Administrative MFA — the sole operator's Supabase administrative account has an enrolled authenticator factor, and the Vercel account has active TOTP two-factor authentication required by the DeCook team. These settings were verified live on August 24, 2026. They support internal-systems MFA but do not constitute centralized workforce IAM.
Encryption
- In transit — TLS everywhere. Vercel terminates TLS and redirects HTTP to HTTPS, and HSTS is enforced with a two-year max-age including subdomains.
- At rest — Supabase encrypts the database and file storage with AES-256.
- Application-layer token encryption — OAuth and Plaid access tokens are encrypted with separate 32-byte keys before they are written to the database, so they are never stored in plaintext even inside an encrypted database.
Browser security headers
Set on every route:
- Content-Security-Policy — generates a fresh unpredictable nonce for every page request, permits only framework and application scripts/styles carrying that nonce, confines outbound requests to an explicit allowlist, and blocks this app from being framed. Production
script-srcandstyle-srcdo not permit'unsafe-inline'. - Strict-Transport-Security —
max-age=63072000; includeSubDomains. Preload is deliberately not enabled, since it is effectively irreversible. - X-Content-Type-Options: nosniff — stops MIME sniffing from turning an uploaded file into executable content.
- X-Frame-Options: DENY and frame-ancestors 'none' — clickjacking defence.
- Referrer-Policy —
strict-origin-when-cross-origin, so document and entity identifiers in a URL never leak to another site. - Permissions-Policy — camera, microphone, geolocation, payment, USB and other unused capabilities are denied outright. The two WebAuthn capabilities (
publickey-credentials-getandpublickey-credentials-create) are delegated to this origin only, so passkey ceremonies work here and cannot be started against Finance OS from inside a cross-origin frame. - Cross-origin isolation — cross-origin opener and resource policies prevent other sites from reusing Finance OS responses as embeddable resources. Browser CORS is limited to the canonical Finance OS origin.
Material Symbols, Inter, and Manrope are self-hosted with the application; the browser does not fetch font CSS or font files from Google.
Audit logging
An append-only audit_events table records sign-in, failed sign-in, sign-out, password reset requested and completed, MFA enrollment, MFA challenge success and failure, document upload, claim review decisions, and OAuth provider connect and disconnect. Each record holds the actor, action, resource, outcome, client IP, user agent, and timestamp.
Append-only is enforced structurally: there is no update or delete policy for signed-in users, and those permissions are revoked at the grant level, so a compromised session cannot rewrite or erase its own trail.
The audit trail never records passwords, session tokens, API keys, OAuth tokens, taxpayer identification numbers, full account numbers, document contents, or claim values — identifiers and outcomes only.
Rate limiting
API routes enforce fixed-window rate limits, keyed on the authenticated user where a session exists and on client IP otherwise. Throttled callers receive a 429 with a Retry-After header.
- Operator diagnostics — 5 requests per minute
- OAuth connect, callback, and disconnect — 10 requests per minute
- Provider status polling — 60 requests per minute
- Audit-event writes from a signed-in session — 60 requests per minute
- Pre-session audit writes (failed sign-in, password-reset request) — 10 requests per minute
Stated limitation: counters are held in memory per serverless instance, so with several instances running the effective ceiling is higher than the nominal limit. This is a meaningful abuse brake, not a hard quota. Sign-in itself is rate limited by Supabase, since the browser calls Supabase Auth directly.
Secrets handling
- No secret is committed to the repository. The example environment file contains placeholders and public values only.
- Runtime secrets are stored as Vercel environment variables, scoped per environment.
- The operator's copies of credentials are held in 1Password — never in plaintext files, chat, or notes.
- Credentials are rotated on suspected exposure, on vendor advisory, and when an integration is decommissioned.
Vulnerability scanning
- Dependency scanning — the locked production dependency graph is scanned with
npm auditon pull requests and weekly. - Source scanning — Semgrep Community Edition scans the TypeScript/JavaScript source on pull requests and weekly.
- Public-web scanning — OWASP ZAP baseline scans the public production surface weekly and on demand using passive vulnerability rules.
- Operational proof — the first post-deploy default-branch run passed dependency, source, and production-web jobs. Dependency and source scans reported zero findings. ZAP scanned 32 production URLs with no failing rules and preserved four medium, one low, and twelve informational categories for follow-up; a passing workflow is not presented as a finding-free scan.
- Remediation windows — critical findings within 7 days, high findings within 14 days, and other supported dependency updates within 30 days.
Stated limitation: Finance OS has no employee or contractor machines, but the sole operator workstation is not covered by a managed enterprise endpoint vulnerability-scanning agent.
What is not yet in place
These are real gaps, listed so that no one has to guess:
- Phishing-resistant MFA — the second factor in effect is TOTP (authenticator app), which defeats password theft but not a real-time phishing proxy. Passkey support is implemented in the application and configured on the project, but the auth provider does not yet serve WebAuthn, so no phishing-resistant factor can be enrolled. Until one can be, this remains a gap.
- MFA at the database layer — MFA is enforced by middleware and API route guards. Row-level security policies do not additionally require AAL2, so the application layer — not RLS — is the MFA boundary.
- Penetration testing — never performed. No formal security audit has been conducted.
- SOC 2 / ISO 27001 — not held, and no audit is in progress.
- Managed operator-workstation scanning — production dependencies, source code, and the public production surface have recurring automated scanners, but the sole operator workstation is not enrolled in an enterprise endpoint vulnerability-management platform.
- Enterprise workforce controls — application RBAC is implemented through a human owner/operator role protected by owner-scoped RLS and mandatory AAL2, plus a narrower non-human agent role with expiring/revocable scopes and hard denials on owner-only consequential actions. A multi-employee job-function hierarchy is not applicable because Finance OS has no workforce. Organization-wide IAM federation, automated employee deprovisioning, and a complete device-aware zero-trust architecture are not claimed.
- Service-role access remains — several server-side modules (document extraction, seeding, tax calculation, reporting, the OAuth token store) still use the Supabase service-role key, which bypasses RLS. RLS is therefore not yet the sole effective boundary for server-side code.
- Session cookies are not httpOnly — sign-in runs client-side, and cookies written by JavaScript cannot carry that flag. This keeps prevention of script injection and strict CSP enforcement especially important.
- Distributed rate limiting — in-memory only, as described above.
- Cryptographically chained audit log — the audit trail is append-only by policy and grant, but is not hash-chained or externally anchored.
- Malware scanning — uploaded documents are not scanned.
- Backup restore testing — Supabase takes automated backups; a restore has never been tested.
- Plaid production availability — the encrypted token store, signed webhook intake, consent, deletion, transaction sync, and audit controls are implemented on the Plaid branch, but they are not a production bank connection and no production bank data has flowed through Finance OS.
Infrastructure providers
Vercel
Application hosting. SOC 2 Type II. Provides automatic HTTPS, DDoS protection, and edge network distribution.
Supabase
Database, file storage, and authentication. SOC 2 Type II. Managed PostgreSQL with AES-256 encryption at rest and automated backups.
Plaid
Bank connectivity. SOC 2 Type II. Sandbox only — no production bank data flows through Plaid at this time.
OpenAI / Anthropic
Optional document extraction using operator-supplied credentials. Document text is submitted for extraction only when a provider is connected, and the connection can be removed at any time.
Responsible disclosure
If you discover a security vulnerability in Finance OS, please contact the system operator directly. Do not disclose vulnerabilities publicly before they have been addressed. Finance OS will remediate security gaps identified by its partners and will update this page to reflect the result.