root@construct:~/projects/topo$
<-- back to /projects

TOPO Contábil

Accounting operations platform for multi-company groups

TypeScriptNestJSNext.jsPrismaPostgreSQLRedis

>Context

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.

>High-level design

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.

Hover or focus a piece: it lights up with its links.
who uses itVercelRailway (production)code and deliveryexternal services1 Accounting team2 AI agents3 DNS and proxy10 GitHub5 NestJS 12 API4 Next.js 16 frontends8 PostgreSQL11 Customer ERP and BI6 Queues and locks13 Assistant model12 Error tracking14 Transactional email7 Workers and crons9 Evidence files
14 pieces14 links6 areas

Components

  • Frontends on Vercel :: Six Next.js 16 apps that are only built when the release script calls each one's deploy hook; the browser talks to the API and sends errors to Sentry and screen usage to PostHog.
  • NestJS API on Railway :: A Docker container with three replicas and a health check. On every request it verifies login, tenant, role and module, and it serves public API v1 (REST and read-only GraphQL) and the MCP connector over HTTP.
  • PostgreSQL with forced RLS :: Reached only through the API, via Prisma 7, with the tenant set on every transaction and the database policy filtering all 120 tenant tables.
  • Queues and workers on Redis :: Bull 4 queues for bulk operations, trial balance import, period sync and email, consumed by the API process, plus locks that stop a cron from running twice.
  • Evidence files on MinIO :: S3-compatible storage on Railway, with each file stored under the owning tenant and served only by the API after a permission check.
  • Assistant on Claude :: The API calls Claude through the Anthropic SDK, with tools that read through the RLS-bound repositories and a monthly cost cap per tenant.
  • Release pipeline :: GitHub Actions runs every PR's checks and a monthly encrypted copy of the database and attachments; a script versions the release, Railway builds the API from its Dockerfile and the frontends ship through Vercel deploy hooks.

>Trade-offs

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.

>Highlights

  • >Five-figure ARR in year one, with zero spend on distribution
  • >Row-level security forced on all 120 tenant tables, behind a no-exemption test
  • >94.7% correct assistant answers on 38 held-out cases, across both repetitions
  • >3,500+ test files and a 9-step gate before every release
  • >372 versioned releases and 2,054 merged pull requests since October 2025
  • >A 3,000-company import went from 262 s to 36 s

>Fun facts

  • *After a machine upgrade, a full cold compile of Topo took 23 seconds on an M4 Max.
  • *Topo's ADRs jump from 004 to 006. Number 005 was never used, and the gap stayed so the others would not be renumbered.
  • *Topo's main branch has 8,382 commits, the first on October 9, 2025.
TOPO Contábil | The Broad Way | Kinho.dev