AAMLabs (now Forge Labs) · Full-stack engineer, backend lead

FortCred Financial

Multi-tenant loan-contract management platform for a finance factory, with 1,100+ contracts and R$1M+ transacted in production.

Client
AAMLabs (now Forge Labs)
Role
Full-stack engineer, backend lead
Period
2024 to present

The problem

A finance factory ran its loan operation on spreadsheets and improvised tools. The volume was already significant, with hundreds of borrowers, thousands of contracts, monthly invoice cycles, and manual reconciliation of late interest and collections. Every manual step was a place where money slipped away or compliance broke.

The system had to cover the full cycle: client onboarding, request approval, automatic installment generation, daily accrual of interest and late fees, collection over WhatsApp, renegotiation, and a per-borrower audit trail in the style of a CRM. Over time it also began serving more than one company on the same database, which brought the requirement to isolate each company’s data.

Architecture

A monorepo with the API in NestJS 11 and the frontend in Next.js 15 with React 19, sharing a PostgreSQL database through Prisma 6. Background work runs on BullMQ with Redis, and WhatsApp messages are sent from the queues.

The system started as single-tenant and grew into a multi-tenant one. Each company has its own clients, contracts, message templates, and credentials, with the payment gateway and WhatsApp instance keys stored encrypted. Isolation happens in two layers: a guard checks that the user belongs to the company in the request, and Prisma injects a companyId filter into every query, so one company never sees another’s data. A user can belong to more than one company through an association table, and a SUPER_ADMIN profile moves across all of them.

   ┌─ Next.js 15 + React 19 ─────────────────────────┐
   │  TanStack Query · Radix UI · Tailwind 4         │
   │  JWT in HTTP-only cookies (withCredentials)     │
   └──────────────────┬──────────────────────────────┘
                      │ axios (typed via Zod)
   ┌─ NestJS 11 API ──┴──────────────────────────────┐
   │  Auth · Clients · Requests · Invoices · Pmts    │
   │  Renegotiations · Communication · Dashboard     │
   │  companyId scoping (multi-tenant)               │
   └──────┬───────────────────────────────┬──────────┘
          │                               │
     PostgreSQL                  BullMQ + Redis ─→ WhatsApp
     (Prisma 6)                  (whatsapp-messages queue)
                                  ├─ random delay 5 to 15s
                                  ├─ 3 retries, exp. backoff
                                  └─ CommunicationLog

The daily jobs (Brazil timezone)

Three routines run in America/Sao_Paulo, in order:

  • 05:00, overdue processing: past-due invoices receive the configured lateFee%, move to UNPAID, and update the request totals.
  • 05:01, late-fee accrual: (lateFeeMora% / 30) × original_value × days_overdue, added daily. Renegotiations always accrue on the request’s original value, not the renegotiated one. It is a non-obvious financial rule, but one that matters for compliance.
  • 06:00, business days only, collection messages: groups overdue invoices and those due today by request and queues the WhatsApp reminders.

Each routine iterates over every configured company and has a manual recovery entrypoint, so a day can be reprocessed without firing accidental side effects.

Domain modeling

The financial state machine maps to a typed Prisma enum: OPEN → APPROVED → ACTIVE → PAID / RENEGOTIATED / UNAPPROVED / LOST. Each request has a recurrence modality (daily, weekly, biweekly, or monthly) that sets the installment frequency. The dashboard reads cached aggregates, with pagination and filters by modality and contract type, which keeps portfolio views fast even with thousands of active contracts.

Access control is by role and by department, with profiles such as LOAN_MANAGEMENT, STREET_SUPERVISOR, and RENEGOTIATION_ANALYST, and every route passes through authentication and role guards.

One thing that took recent work was invoice settlement. Partial payment, payoff, and refund all have to keep the request balance exact in any order of operations. A double-counting bug corrupted amountDue in certain refund scenarios, and the fix required making settlement atomic. Today payment write-offs are recorded inside the system itself, and the AbacatePay integration, to confirm payments automatically, is being adopted. The lesson: in a financial system, the order and atomicity of balance operations are not a detail, they are the product.

WhatsApp collection: from Evolution API to the official API

The collection channel is WhatsApp, and the company is always the one starting the conversation: due-date reminder, overdue notice, collection. The first version used Evolution API, an unofficial bridge over WhatsApp. It served well to validate the product, but it has a clear ceiling. Because the contact always comes from the company, and at volume, that pattern runs into WhatsApp’s rules and leaves the number under constant risk of being blocked. For an operation that depends on daily collection, that is not viable.

That is why the migration in progress is to the official WhatsApp API, Meta’s Cloud API. It exists precisely for this case: messages initiated by the company go through templates approved in advance, which makes sending at scale legitimate and stable. The current modeling already eases the transition, because collection and reminder messages are already treated as per-company templates rather than free text, which maps directly to the approved-template concept of the official API.

The lesson here is about product, not only technology: an unofficial integration is great for proving an idea, but any communication channel that becomes a dependency of the business needs to move to the official path before it scales.

Reliability

As the system became the backbone of a real operation, the test and documentation coverage followed. There are now more than 300 tests, between unit and integration, covering the financial services, the guards, and the jobs, plus OpenAPI documentation on every controller. It is not coverage vanity: in interest, late-fee, and settlement calculations, the test is what lets you touch the code without fear of breaking someone’s balance.

Result

In production and under continuous maintenance. The factory operates entirely inside the system, with no spreadsheets, and the queues handle the daily cycle without human intervention. The base grew from one company to several on the same instance, and I keep maintaining and evolving the product. The numbers below are live counts, not estimates.

Numbers

  • R$1M+

    Transacted

  • ~20K

    Invoices generated

  • 1,100+

    Contracts registered

  • 394+

    Clients onboarded

Stack

  • NestJS 11
  • Next.js 15
  • React 19
  • Prisma 6
  • PostgreSQL
  • BullMQ
  • Redis
  • WhatsApp Cloud API
  • AbacatePay
  • JWT + HTTP-only cookies
  • TanStack Query
  • Zustand
  • Radix UI
  • Tailwind 4