Topo is a modular platform that runs the accounting operation of multi-company groups on top of their ERP: account reconciliation with review, approval and business-day deadlines, close management, tax obligations and clearance certificates, financial statements, external audit and consolidation. The goal is to give that operation standardization, control, visibility and traceability.
I co-founded Topo in October 2025 with an accountant. I built the product architecture, engineering and security. It now has 8 product modules, including an assistant on Claude.
The data is corporate finance data, so tenant isolation lives in the database, and the audit trail is append-only. Topo reached five-figure ARR in its first year from paying B2B customers, with zero spend on distribution.
The six frontends (app, admin panel, marketing site, docs, developer portal and wiki) are Next.js 16 apps on Vercel. The API is a NestJS 12 service in a Docker container on Railway, with three replicas running, and PostgreSQL, Redis and MinIO also run on Railway. Cloudflare serves the domain's DNS and sits in front of the API.
The browser loads the screen from Vercel and then asks the API for data directly, with the session in a cookie the page code cannot read. ERPs and BI tools come in through public API v1 with an API key, and AI agents through the MCP connector with OAuth, served by the same API process. The API checks login, tenant and role, and every PostgreSQL query carries the tenant in its transaction so row-level security hides other tenants' rows. No frontend talks to the database, and attachments live in MinIO under the owning tenant. Trial balance imports, bulk operations, period sync and emails run on Bull queues in Redis, consumed by the API process itself; scheduled jobs take a Redis lock so they never run twice. The assistant calls Claude through the Anthropic SDK, and errors from the API and the frontends go to Sentry.
Push and merge publish nothing. A release script writes the version into a file in the repo and pushes it, and Railway only builds the image when that file changes. Before the new API starts, a one-off step syncs the schema and reapplies RLS; if any tenant table is left without the policy, the release stops and the new API never starts. In production the API goes first, and the script only calls the Vercel deploy hooks for the frontends once the API reports the new commit. Staging is a separate environment with one replica and its own data, and production only receives what has already been through staging, emergency fixes aside.
Isolate tenants in the database, with row-level security enabled and forced on every table that carries a tenant.
Alternative rejected: Rely only on the tenant filter written into each application query.
Why: One forgotten WHERE in one query would show one company's books to another. The database policy applies to every query, including the ones nobody remembered to review. Without FORCE, the table owner would bypass its own policies.
A test that fails if any tenant model lacks RLS enabled, forced and with a policy, with no exemption list.
Alternative rejected: A list of tables excused from the rule, each justified at the time.
Why: An exemption list grows with items that each look defensible and never get revisited. A table that needs a different rule gets its own policy instead of none.
Result<T, E> and ValidationResult with severity levels in the domain and application layers.
Alternative rejected: Exceptions and try/catch for business failures.
Why: It separates expected failures from system errors, makes handling explicit at every call site and lets the screen explain why something was refused, such as the same person preparing and approving a reconciliation. The cost is more verbose code and controllers that must unwrap results.
Clean Architecture with a dependency-free domain and layer boundaries checked in CI.
Alternative rejected: Business rules written straight into framework services and ORM calls.
Why: Accounting rules are tested without a database or a framework. The price is ceremony: a simple feature takes several files and a mapper layer between domain, database and DTOs.
A new module ships dark: it is in the catalog and the contract, but has no route or screen until it is turned on, and access is denied by default.
Alternative rejected: Make the module visible as soon as its code reaches production.
Why: The commercial side (packages, pricing, per-company grants) moves without waiting for engineering to finish, and no customer sees a half-built module.