Background
Personal spending, a mortgage, overdrafts and money lent on to partners were scattered across statements from several banks and a few spreadsheets. Every month meant reconciling by hand: what had been paid, how much interest, how much partners still owed. SPARK Finance brings it all together — and was then split into a multi-tenant SaaS so others can use the same system.
Key features
- AI statement import: accepts PDF, images, CSV and XLSX. The browser splits PDF pages (pdf.js) / converts XLSX to CSV and uploads files straight to Cloudflare R2 with presigned URLs; an LLM reads each page (~6 seconds/page) and produces a draft — nothing is written to the ledger until the user clicks Import. Duplicate imports are blocked by sha256, balances are checked for continuity across periods, and imports run in batches.
- Transactions & categorization: categories, auto-categorization rules (preview, then apply), trends by category, monthly cash flow, income sources and fixed costs.
- Loans & overdrafts: outstanding balances, estimated interest, overdraft draw/repay tracking and cash-flow reconciliation.
- Partner lending: daily interest
balance × rate/365 × days, multiple rate periods, manual adjustments, rebuilding from transactions. Partners sign in with a 6-digit code and report repayments via VietQR; the lender confirms through a Telegram bot. - Linked tenants: the lender invites the borrower; the borrower sees their own loans, and both sides reconcile loans and transfers automatically (±3 days tolerance).
- Drag-and-drop dashboard: 9 widget types, plus chart widgets the AI assistant builds from a single prompt.
- AI assistant: SSE chat via SPARK AI Gateway to a dedicated OpenClaw agent; the agent may only call read tools, and every write goes through a confirmation card.
Multi-tenant SaaS
- One subdomain per tenant
<tenant>.finance.devbyspark.com; the tenant is only ever determined from the subdomain, an authenticated API key or a server-signed session — never from headers, query strings or request bodies. - 4 layers of isolation: a dedicated
tenant.IDtype, a connection pool that refuses queries without a tenant, defaults +WITH CHECKfrom PostgreSQL Row-Level Security, and RLSUSINGclauses. The tenant travels incontext.Context, so ~55 repository functions kept their signatures; 14 foreign keys moved to composite(tenant_id, …)keys. - Automatic tenant provisioning: a superadmin creates the tenant, the backend commits the subdomain's Ingress to the GitOps repo via the GitHub API, ArgoCD syncs and cert-manager issues TLS.
- Roles: superadmin, tenant admin (6-digit code) and partner (separate access code); AI on/off and AI message quotas per tenant.
MCP for external agents
- Endpoint
https://<tenant>.finance.devbyspark.com/api/mcp— Streamable HTTP, JSON-RPC 2.0, with sessions. - 28 tools (24 read, 5 write; one semantic-search tool is hidden when embeddings are off): transactions, cash flow, categories, loans, overdrafts, partners, import drafts… — one registry shared by REST, MCP and the AI assistant.
spk_live_…API keys are stored only as SHA-256, with permissions chosen per tool and an expiry, valid only on the tenant's own subdomain, 120 calls/minute.- Sensitive operations (changing balances, recording lending periods) require
confirm=true; writes carry anidempotency_key; every session and call is logged for 90 days. The "Connect MCP" screen generates ready-made commands/prompts for Claude Code, Claude Desktop, Cursor and OpenClaw.
Engineering
- Backend: Go 1.22 with only 3 direct dependencies (pgx, minio-go for R2, x/crypto); the router is the standard library's
http.ServeMux(~126 routes); a home-grown migrator with checksums and dry-run (37 migrations); distroless image. - Database: PostgreSQL with Row-Level Security,
pg_trgmfor search,pgvectorfor the assistant's semantic memory. - Frontend: React 18, Vite, TypeScript, MUI + MUI X, ApexCharts, react-grid-layout, pdf.js, SheetJS.
- Operations: GitHub Actions → Docker Hub → GitOps repo → ArgoCD on Kubernetes (K3s), cert-manager + Let's Encrypt, zero-downtime rolling updates.
- Testing: 300+ Go tests, each running in its own Postgres schema; tests that lock in the money logic (reproducing all 12 real lending periods); 31 multi-tenant end-to-end scenarios over HTTP with each tenant's Host header.
Results
- Replaced spreadsheets with a self-reconciling financial ledger: lending figures match the original spreadsheet on 12/12 periods.
- Running for 3 tenants on
finance.devbyspark.comand their own subdomains. - AI agents (Claude, OpenClaw…) read and write financial data over MCP with per-tool permissions and a safe confirmation step.
Highlights
Moved from a single-owner app to a multi-tenant SaaS without changing the signatures of ~55 repository functions: the tenant travels in the context, with 4 layers of isolation backed by PostgreSQL Row-Level Security (22 RLS test cases).
LLM statement import: PDF/image/CSV/XLSX uploaded straight to R2 via presigned URLs, page-by-page OCR (~6 s/page), sha256 de-duplication, running balance checks, and nothing written until the user clicks Import.
Streamable HTTP MCP server per tenant: spk_live keys with per-tool permissions and expiry, mandatory confirmation before changing balances, 90-day logs; one tool registry shared by REST, MCP and the AI assistant.
Partner lending: daily interest, multiple rate periods, partners report repayments via VietQR and the owner confirms via a Telegram bot; matches the real spreadsheet on 12/12 periods.
Links lender and borrower tenants with two-way reconciliation of loans and transfers.
Adding a tenant generates its Ingress in the GitOps repo via the GitHub API; ArgoCD syncs and issues TLS for the subdomain.
