Back to Blog

Fintech Web Development: Building Platforms That Handle Real Money

Early in a recent project, a client asked us to add a "quick balance calculation" to their trading dashboard using JavaScript's standard number arithmetic. We declined. Not because it was hard to write — it takes two minutes — but because JavaScript'

Viprasol Tech Team
March 11, 2026
9 min read

Fintech Web Development: Building Platforms That Handle Real Money | Viprasol Tech

Fintech Web Development: Building Platforms That Handle Real Money

By Viprasol Tech Team | Fintech & Web Development


Early in a recent project, a client asked us to add a "quick balance calculation" to their trading dashboard using JavaScript's standard number arithmetic. We declined. Not because it was hard to write — it takes two minutes — but because JavaScript's floating-point representation of 0.1 + 0.2 is 0.30000000000000004. On a dashboard showing portfolio values to thousands of users, that kind of error compounds into real discrepancies.

We spent five minutes explaining this. The client switched from "I need this today" to "what's the right way to do this?" That's the conversation that separates fintech web development from building a blog.

When your web application handles real money — portfolio values, executed trades, payment flows, lending balances — the engineering standards are categorically different. The same technologies apply (Next.js, PostgreSQL, AWS), but the architectural decisions and non-negotiable constraints are distinct. This is what those constraints look like in practice.


Money Arithmetic Is Not Optional

Every financial value in a fintech application must be stored and calculated as integers representing the smallest currency unit. Paise for rupees, cents for dollars, satoshi for bitcoin.

// Never do this for financial values
const total = 100.10 + 200.20;  // 300.29999999999995 in JavaScript

// Store as integers — always
const amount1Paise = 10010;   // ₹100.10
const amount2Paise = 20020;   // ₹200.20
const totalPaise   = amount1Paise + amount2Paise;  // 30030 — exact

// Display formatting is a presentation concern, not a storage concern
const formatINR = (paise: number) => 
  new Intl.NumberFormat('en-IN', {
    style: 'currency',
    currency: 'INR',
    minimumFractionDigits: 2
  }).format(paise / 100);

For complex calculations — interest accrual, compound returns, multi-step portfolio rebalancing — use a decimal arithmetic library. We use decimal.js for Node.js backends:

import Decimal from 'decimal.js';

// Compound interest: no floating-point drift across 360 iterations
function calculateCompoundValue(
  principalPaise: number,
  annualRatePct: number,
  days: number
): number {
  const principal = new Decimal(principalPaise);
  const rate      = new Decimal(annualRatePct).div(100).div(365);
  const result    = principal.times(rate.plus(1).pow(days));
  return result.toDecimalPlaces(0).toNumber();  // round to nearest paise
}

This decision needs to be made at the schema design stage. A balance DECIMAL(15,2) column in PostgreSQL and a JavaScript Number in the API layer will accumulate floating-point errors across enough operations. Use BIGINT for monetary amounts in the database. Store as integers. Divide only at the display layer.


Immutable Audit Trails Are the Foundation

Every financial operation — deposit, withdrawal, transfer, trade execution, fee deduction — must generate an immutable record that can be audited by regulators, reconciled by accountants, and replayed for debugging.

The pattern we use is a ledger event table that is append-only. No updates. No deletes. Corrections are new events:

CREATE TABLE ledger_events (
  id            UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
  event_type    TEXT        NOT NULL,  -- 'deposit', 'withdrawal', 'trade_fill', 'fee', 'reversal'
  account_id    UUID        NOT NULL REFERENCES accounts(id),
  amount_paise  BIGINT      NOT NULL,  -- positive = credit, negative = debit
  balance_after BIGINT      NOT NULL,  -- snapshot for quick audit without replaying history
  reference_id  TEXT,                  -- external reference (payment gateway ID, order ID)
  metadata      JSONB       NOT NULL DEFAULT '{}',
  created_by    UUID        REFERENCES users(id),
  ip_address    INET,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Revoke UPDATE and DELETE from application role
REVOKE UPDATE, DELETE ON ledger_events FROM app_user;

-- Performance indices
CREATE INDEX idx_ledger_account_time ON ledger_events(account_id, created_at DESC);
CREATE INDEX idx_ledger_reference    ON ledger_events(reference_id) WHERE reference_id IS NOT NULL;

No updated_at column. No soft-delete flag. This table only grows. When a client asks "what happened to my balance on March 14th at 3:47pm?" — the answer is immediately available and cannot be disputed, because the records are structurally immutable.

If a transaction was entered incorrectly, you don't update the row. You create a reversal event (negative amount) and a corrected replacement event, both with audit metadata explaining the correction. This is how bank accounting works, and it's how fintech applications should work.


🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Transactional Integrity: Both Sides of Every Transfer

Fund transfers between accounts must be atomic. Either both the debit and the credit happen, or neither does. Database-level transactions enforce this guarantee:

async function transferFunds(params: {
  fromAccountId: string;
  toAccountId: string;
  amountPaise: number;
  initiatedBy: string;
  note?: string;
}): Promise<void> {
  await prisma.$transaction(async (tx) => {
    // 1. Fetch and lock both accounts to prevent concurrent modification
    const [from, to] = await Promise.all([
      tx.account.findUniqueOrThrow({ where: { id: params.fromAccountId } }),
      tx.account.findUniqueOrThrow({ where: { id: params.toAccountId   } }),
    ]);

    if (from.balancePaise < params.amountPaise) {
      throw new Error(`Insufficient balance: ${from.balancePaise} paise available`);
    }

    const newFromBalance = from.balancePaise - params.amountPaise;
    const newToBalance   = to.balancePaise   + params.amountPaise;

    // 2. Update both balances
    await tx.account.update({
      where: { id: params.fromAccountId },
      data:  { balancePaise: newFromBalance }
    });
    await tx.account.update({
      where: { id: params.toAccountId },
      data:  { balancePaise: newToBalance }
    });

    // 3. Log both ledger events in the same transaction
    await tx.ledgerEvent.createMany({
      data: [
        {
          accountId:    params.fromAccountId,
          eventType:    'transfer_debit',
          amountPaise:  -params.amountPaise,
          balanceAfter: newFromBalance,
          createdBy:    params.initiatedBy,
          metadata:     { toAccountId: params.toAccountId, note: params.note }
        },
        {
          accountId:    params.toAccountId,
          eventType:    'transfer_credit',
          amountPaise:  params.amountPaise,
          balanceAfter: newToBalance,
          metadata:     { fromAccountId: params.fromAccountId, note: params.note }
        }
      ]
    });
  });
  // Prisma rolls back the entire transaction automatically if any step throws
}

If this function fails halfway through — server crash, database connection drop, anything — the transaction is rolled back. No partial transfers. No "credited but not debited" bugs that show up in reconciliation three weeks later.


Real-Time Data: WebSocket Architecture That Scales

A trading or portfolio dashboard without live data isn't competitive. Users expect their balance to update when a trade executes, not on the next page refresh. The architecture we use:

User Browser
│  WebSocket connection (wss://api.platform.com/ws)
│
Node.js WebSocket Server (ws library + custom auth middleware)
│
├── Redis Pub/Sub — subscribes to per-user and per-symbol channels
│   ├── channel: user:{userId}:portfolio     → equity updates
│   ├── channel: price:{symbol}              → live price ticks
│   └── channel: user:{userId}:orders        → order status changes
│
Price Feed Service (separate process)
├── Connects to data vendor (e.g., Polygon.io, broker WebSocket)
├── Normalizes tick data
└── Publishes to Redis price channels
│
Portfolio Calculator Service
├── Listens for price updates
├── Recalculates floating P&L for open positions
└── Publishes updated portfolio state to per-user channels

The decoupling through Redis is important. The WebSocket server doesn't know anything about price feeds or portfolio calculation — it just relays Redis messages to connected clients. Adding a new consumer (mobile push notifications, alerting service) means subscribing to the same Redis channel, with no changes to existing services.

One thing we always add: throttling on the client side. A busy equity index can tick 20+ times per second. Broadcasting every tick to 5,000 connected clients is wasteful. We throttle dashboard updates to one per 500ms per user, with a flag for "full tick" mode that clients can request for professional users.


🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Authentication and Session Security

Financial applications need stronger authentication than a 30-day JWT with no rotation. The pattern we implement:

Short-lived access tokens (15-minute expiry) paired with refresh token rotation. When a refresh token is used, it's immediately invalidated and a new pair is issued. If an attacker steals a refresh token, using it will invalidate the session — the legitimate user's next request will fail, alerting them that something is wrong.

TOTP two-factor authentication is mandatory for financial applications. We implement via the otplib library — TOTP compatible with Google Authenticator, Authy, and any standard authenticator app.

Database-level row security as a defense-in-depth layer, per the PostgreSQL row security documentation:

-- Even if application code forgets the WHERE clause, the database enforces isolation
ALTER TABLE accounts       ENABLE ROW LEVEL SECURITY;
ALTER TABLE transactions   ENABLE ROW LEVEL SECURITY;
ALTER TABLE portfolio_items ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_data_isolation ON accounts
  USING (user_id = current_setting('app.current_user_id')::UUID);

The OWASP API Security Top 10 is worth reviewing for any team building financial APIs — the broken object-level authorization vulnerability (accessing another user's resources by guessing IDs) is the most common serious security issue we find in fintech API reviews.


Payments: What "Handling Payments" Actually Requires

If your platform takes payments, you do not handle raw card data. Full stop. Use Stripe's hosted fields or equivalent — the card data never touches your servers, and you avoid the full weight of PCI-DSS compliance.

What you are responsible for: securing your Stripe secret key (AWS Secrets Manager, not environment variables in your repository), validating Stripe webhook signatures before processing events, idempotency in webhook handlers (Stripe may deliver the same event more than once), and building a proper state machine for subscription lifecycle events (trial_end, payment_failed, invoice_paid, subscription_canceled).

The subscription state machine is where most fintech SaaS billing bugs live. What happens when a payment fails and the user is mid-session? Do they lose access immediately? After a grace period? After the third retry? These decisions need to be made, implemented, and tested before launch.


Compliance for Indian Fintech Products

Platforms processing payments in India need to account for RBI guidelines on digital payments. For consumer-facing products: KYC requirements (Aadhaar-based or Video KYC), transaction limits for semi-KYC accounts, and GST on subscription revenue (18% for B2B services to Indian customers).

For products serving EU users under GDPR: the right to erasure requires a deletion workflow that cascades across all services and databases. For financial records, you cannot simply delete transaction history — instead, pseudonymize the user identifier while preserving the financial record structure.


What We Build at Viprasol

Our fintech web development work spans trading dashboards, prop firm management platforms, payment processing integrations, and portfolio analytics tools. We've applied the architecture described in this post across projects for clients in the UK, UAE, Europe, and across India.

Every financial application we deliver includes decimal-safe money handling, immutable ledger architecture, atomic transaction management, row-level security, and short-lived JWT authentication. We also work on algorithmic trading software for clients who need the web layer to connect directly with MT5 or custom trading infrastructure.

If you're building a fintech product and want a team that understands why these architectural choices matter, start with a conversation.


See also: Custom SaaS Development in India — What to Expect

Sources: Stripe Security Documentation · OWASP API Security Top 10 · PostgreSQL Row Security

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.