Finvica — WealthTech Platform
ProsperTech · Full Stack Lead, 2026 – Present · Visit
- Stage
- In production with early customers
- Team
- A core team of fewer than five engineers
Advisory firms run their book across spreadsheets, RTA portals and a different tool per asset class. Nobody can answer what a client actually holds without reconciling four sources by hand — and in this domain a number that is close is the same as a number that is wrong.
What the design had to survive
- The consistency boundary wraps most of the platform
- An order becomes a transaction, the transaction moves a position, the position is valued against a NAV, a corporate action rewrites it, and a distributor’s commission falls out of all of it. One backdated entry reaches every step, so they cannot be five services without becoming a distributed transaction.
- It ships into other people’s data centres
- Multi-tenant SaaS and white-labelled enterprise deployments, some inside firms with their own infrastructure policies. Two dozen services there is not a harder deployment — it is a different product, with a different support contract.
- The upstream systems cannot be changed
- RTAs, KRAs, exchanges and custodians. Fixed-width files over SFTP, a specification last revised in 2016, webhooks that may not arrive, and two authoritative systems that sometimes disagree about the same holding.
- Corrections are normal, not exceptional
- NAV files land late, arrive twice and get revised. Corporate actions are re-sent, re-rated and occasionally cancelled after announcement. Any design that treats the feed as truth on arrival breaks in week two.
- A small team against deep domain complexity
- The bottleneck was never coordinating deploys. It was that AIF drawdowns, bond coupon accrual and SIP mandates all behave differently, and being wrong about any of them is unacceptable.
Four decisions shaped this platform more than the rest. Each is summarised here and argued properly in a separate piece — this page is the map, not the territory.
One system, not two dozen services
Written out, the scope invites the question: identity, tenancy, onboarding and KYC, master data, orders, transactions, corporate actions, holdings and analytics, reporting, revenue, CRM, advisory. That reads like an architecture diagram with a lot of boxes in it.
It is one NestJS application, one Postgres database, two processes. The reason is that the middle of that list isn't five domains that occasionally talk — it's five views of one thing, and a single backdated transaction reaches all of them. Splitting them turns a database transaction into a distributed one, and the standard answer, a saga with compensating actions, means accepting a window in which a relationship manager can open a screen and read a number that isn't true.
The split we did make is on operational profile rather than domain nouns: an API server and a headless worker, same codebase, different entry point. Bulk uploads and report generation have no business competing with a dashboard request.
Corporate actions are ledger entries, not updates
A 1:1 bonus doubles the unit count and halves the NAV. Apply the units on the ex-date morning and take the revised NAV from the evening feed, and for most of a working day a client's portfolio shows double its actual value. Advisors saw it. One of them called.
Nothing had thrown an error. The mistake was conceptual: treating a corporate action as an update to a number rather than an event with its own announcement, ex-, record and payment dates, each governing something different.
So every action is an entry in the transaction ledger and holdings are derived by replaying it. Corrections become another entry rather than an archaeology problem. Each action carries a natural key of scheme, type and ex-date behind a unique constraint, because feeds re-send, and applying a 1:1 bonus twice quadruples a holding without looking obviously wrong.
Nothing external writes to a domain table
The integration specification I spent the most time with was a PDF last revised in 2016, describing a fixed-width file over SFTP, with a field marked "reserved for future use" that turned out to contain something important.
These systems are load-bearing for the entire industry and they are not going to change for us. So the rule that survives everything else: external data lands raw, in its own table, exactly as received — then transformation into domain tables is a separate, re-runnable step. When a registrar's format shifts or a mapping turns out to be wrong, the fix is a re-transform rather than a reconstruction of state nobody kept.
Webhooks are treated as a hint that something changed, never as delivery; the reconciling poll is the real mechanism.
Reconciliation is a feature, not a support ticket
An advisor called because a client's portfolio showed ₹4,18,000 in our dashboard and ₹4,17,600 on the RTA's statement. Four hundred rupees, on a portfolio where it rounds to nothing, and the only thing either of them wanted to discuss.
They were right to care — nobody trusts a number that's close. One scheme had been valued at the previous day's NAV because that day's file landed after the valuation job ran, and nothing went back to fix it.
The fix took an hour. Working out why the system permitted it took considerably longer, and the answer was that breaks needed to be a first-class object with a screen, not something an engineer discovers in a log. Imports are all-or-nothing, a holding is only valued against a NAV whose date is consistent with its unit count, and where the two are out of step the position reads "pending revaluation" rather than showing a number.
Between 95 and 100% of transactions now match the registrar on the first pass. The rest are the point: they don't fail quietly, they land on a breaks screen with both figures side by side and a date attached, which is the only version of this an advisor can actually do anything about.
Every advisor I've asked prefers that to finding out from a client.
What I'd change
Postgres took about ten minutes to choose. The ORM is still generating consequences, and it's the decision I'd want back — not to reverse, but to have understood earlier.
The schema is TypeScript, migrations are generated from it, and there is no repository layer. I'd take that trade again: one representation of a table, types the compiler checks against the same definition the migration came from, and no abstraction maintained forever in case somebody someday swaps the database.
What I underestimated is the bill. The migration generator emits only what its
DSL can describe, and five categories of Postgres object fall outside it —
trigger functions and their triggers, most raw CHECK constraints, row-level
security and its policies, and any partial or expression index. Look at what
that list actually contains here: tenant isolation, the append-only guarantee
on attribution, the write-once guarantee on the audit tables. The most
important integrity properties in the system are precisely the ones the schema
language cannot say.
Worse than silence is a near miss. For eight unique indexes the generator emits
something — right name, right columns, and quietly without the WHERE clause
or the lower() that made the index correct. A unique index on tenant and
email losing its WHERE email IS NOT NULL means only one staff row per tenant
may have a null email, which is a plausible bug report months later in a
component nobody would connect to a migration.
A missing table is loud; the next query throws. Every object in that file fails silently — the database has every table, every column, every foreign key, seeding completes, tests pass, and what's actually gone is that tenant-scoped queries return rows, just not only this tenant's rows. So those objects live in a hand-maintained SQL file with a check mode that reports drift, run in CI and after every deploy.
That guard works. But I'd rather have designed for it on day one than discovered the need for it by auditing eight indexes that looked fine.
The system today
- advisory firms live
- 5
- auto-reconciled, first pass
- 95–100%
- to onboard a firm, fully set up
- 1–2 days
- asset classes
- 12+