Skip to content
HL
Available
Case study / fintechshipped

Finance OS

Self-hosted multi-currency finance PWA with a Claude advisor on AWS Bedrock

At a glance

Platforms
Web, installable PWA
Timeline
April 2026 (32 commits, 24–26 April)
Team
Solo
Codebase
32 Prisma models, 36 API route handlers, 24 App Router pages
Tests
99 Vitest tests, run in GitHub Actions on pushes and PRs to main
Source
Public repo: github.com/Hmz1hb/finance-os

Overview

Finance OS is an open-source, self-hosted progressive web app for running personal and business finances side by side. It was built for freelancers and small-company owners who earn and spend in several currencies (MAD, GBP, USD and EUR) and run a company alongside their personal finances. It covers transactions, income schedules, receivables, owner pay, payroll, recurring rules, subscriptions, loans, goals, net worth, tax reserves and reports, and adds a streaming Claude advisor and receipt OCR on AWS Bedrock. I designed, built, deployed and tested it on my own.

The challenge

Off-the-shelf budgeting tools assume one currency and one household. This use case needed four currencies with the Moroccan dirham as the reporting currency, and a strict boundary between company money and personal money, since owner pay moves between the two. It also needed configurable UK corporation-tax and Moroccan auto-entrepreneur estimates, and cashflow that counts money not yet received. Because the data is private, the app had to be self-hosted for a single user, keep receipts in private storage, and call an LLM without storing cloud access keys in the app. Most money bugs are silent, so totals had to agree between pages, and money movements with several steps had to be atomic.

My role

I owned the whole project: the Prisma domain model (32 models, three migrations), 36 Next.js route handlers, 24 App Router pages and the UI components, and the Bedrock integration for the advisor and receipt OCR. I also built the S3 attachment storage, the auth and request-checking proxy, the PWA setup, the Vitest suite and CI. Deployment was mine too, on AWS: EC2 with Docker Compose, runtime config from SSM Parameter Store, deploys from GitHub through OIDC, and nightly Postgres backups to S3. After deploying, I ran ten documented rounds of agent-driven QA against the live app, logged in the repo's qa/ folder, then fixed and re-verified the findings in production.

Architecture

  1. Clients

    • Installable PWA

      Next.js 16 App Router, React 19, Tailwind CSS v4, Recharts, Workbox (@ducanh2912/next-pwa)

      Server-rendered dashboards with separate personal and business views, an entity rail, charts and an offline fallback page.

  2. Edge/Delivery

    • Cloudflare Tunnel

      cloudflared container

      Publishes the app through an outbound tunnel. The app container exposes port 3000 only on the internal Compose network.

  3. Services

    • Request proxy

      Next.js 16 proxy (src/proxy.ts), NextAuth v5

      Enforces the session, checks Origin against Host on API writes, accepts only JSON or multipart bodies on writes, and rate-limits writes per user and login attempts per IP.

    • Finance API

      Next.js route handlers, Zod 4, Prisma 7

      36 handlers covering transactions, receivables, owner pay, payroll, recurring rules, goals, loans, subscriptions, tax estimates and CSV import. They all validate input with one set of shared Zod schemas.

    • Domain logic

      TypeScript modules (cockpit, cashflows, tax, recurring, health, net-worth)

      Computes balances across entities, instances of recurring rules, an emergency-fund projection, a financial health score, and configurable UK corporation-tax and Moroccan auto-entrepreneur estimates.

  4. Media

    • Receipt storage

      AWS S3, presigned URLs

      Keeps attachments in a private bucket with a lifecycle rule. Files are served only through short-lived presigned URLs.

  5. Data

    • PostgreSQL

      PostgreSQL 16, Prisma 7 with the pg adapter

      Stores amounts as integer cents, along with the exchange rate and MAD equivalent at write time. It uses soft deletes and keeps a daily exchange-rate table.

  6. Third-party

    • Exchange rates

      open.er-api.com

      Fetches rates against GBP once a day, expands them into a full MAD/GBP/USD/EUR matrix and caches it in Postgres. Rates can also be set by hand.

    • AI advisor and receipt OCR

      AWS Bedrock (Claude Sonnet), InvokeModelWithResponseStream

      Streams advisor replies based on a compact snapshot of the user's finances, and turns receipt images into structured JSON.

  7. Infrastructure

    • Hosting and delivery

      EC2, Docker Compose, SSM Parameter Store, GitHub Actions OIDC

      When CI passes on main, the deploy workflow uploads a source tarball to S3, and an SSM command rebuilds the app on the host. A cron job writes a nightly pg_dump backup to S3.

Key decisions

  1. 01

    Integer cents with a per-row FX snapshot

    Every transaction stores amountCents, its currency, the exchange rate used and a madEquivalentCents value fixed when the row is written. Historical reports therefore don't change when rates change, and totals agree across pages. The cost is extra columns on every money table, and all currency conversion has to go through one money module, which has its own tests.

  2. 02

    Entity-led domain model

    Early in the build, I redesigned the model so that financial entities (for example a company and a personal entity) sit at its core. Receivables, expected income, owner compensation, tax reserves and recurring rules each belong to an entity. This keeps business and personal money separate, and makes owner pay an explicit flow between a business entity and a personal entity instead of two unlinked transactions.

  3. 03

    Atomic multi-step money flows

    The owner-pay flow writes four records: a business expense, a personal income, a payroll-person upsert and a payroll payment. All four run inside one interactive Prisma $transaction, so if any step fails the others roll back. A dedicated test covers the rollback.

  4. 04

    Claude on Bedrock with IAM-role credentials

    The advisor and the OCR call Claude through AWS Bedrock using role-based AWS credentials, so the app stores no AWS access keys. The advisor's system prompt carries a compact snapshot of the user's finances instead of raw tables, which keeps each request small. If a Bedrock call fails, the AI routes return a fallback response instead of an error.

  5. 05

    Keyless FX with a cached rate matrix

    The ECB-backed Frankfurter API doesn't list MAD, so I pull rates from open.er-api.com, which needs no API key. One fetch against GBP each day is expanded into all 16 currency pairs, which are upserted in a single transaction. If a refresh fails, the app keeps using the last cached rates, and rates can also be set by hand.

  6. 06

    Single-host AWS deploy without long-lived secrets

    I run the app as Docker Compose on one EC2 instance behind a Cloudflare Tunnel, not on a managed container service. GitHub Actions gets AWS access through OIDC, and runtime config comes from SSM Parameter Store, so no long-lived AWS keys are stored in GitHub. This is simple and cheap for a single user. The trade-offs are no horizontal scaling and a rate limiter that keeps its counts in memory.

Results

  • Built a multi-currency finance PWA in three days (24–26 April 2026). It covers income, expenses, payroll, receivables, recurring rules, subscriptions, loans (with a snowball toggle), goals, net worth, tax reserves and reports.
  • Added a streaming Claude advisor and receipt OCR on AWS Bedrock.
  • Wrote 99 Vitest tests covering money math, recurrence, the shared validation schemas, the CSRF and rate-limit checks, the cap on goal contributions, upload size pre-checks and the owner-pay rollback.
  • Set up CI that runs lint, typecheck, tests, the production build and a Docker build on pushes and PRs to main. A separate deploy workflow runs only after CI passes and ships main to AWS.
  • Ran ten documented QA rounds against the deployed app and verified the fixes live. The fixes included shared validation for POST and PATCH requests and cursor pagination for transactions.
  • Published the project as a public repository with setup docs, secrets documentation and infrastructure scripts.

Stack

Frontend
Next.js 16 (App Router)React 19TypeScriptTailwind CSS v4RechartsSonnerlucide-reactWorkbox PWA
Backend
Next.js route handlersNextAuth v5 (credentials)Zod 4date-fnsbcryptjs
Data
PostgreSQL 16Prisma 7
AI
AWS BedrockClaude Sonnet (streaming + vision OCR)
Cloud & DevOps
AWS EC2AWS S3AWS SSM Parameter StoreDocker ComposeCloudflare TunnelGitHub Actions (OIDC)
Testing
VitestTesting Libraryjsdom