Introduction

FinVault is a multi-tenant SaaS platform for issuing, managing, and verifying blockchain-anchored digital certificates on the Polygon network. Organisations (universities, training providers, corporates) use FinVault to issue tamper-proof credentials to recipients, who can share and verify them via QR code or certificate ID — permanently anchored on-chain.

Document Purpose

This document is the production handover reference for software developers and DevOps engineers taking the FinVault repository to production. It has two reader tracks:

Use the Track dropdown in the top navbar to filter the sidebar to your track. All content is accessible regardless of track selection.

Version & Status

FieldValue
Versionv1.1.0
Git commit4d626e6 (2026-06-03 security-hardened release)
Node.js20.x
Polygon networkAmoy testnet (mainnet pending CertiK audit)
GitHubgithub.com/mail2ratnakar/finvault

Phase Status

PhaseDescriptionStatus
Phase 1 — MVPCore issuance, blockchain anchoring, public verification, KYC, billing✅ Complete
Phase 2 — Pre-launchRecipient wallet, DigiLocker, advanced analytics, helpdesk, multitenancy🔄 In Progress
Phase 3 — ScaleSDK, white-label, bulk API, enterprise SLAs, SSO/SAML⏳ Not Started
How to navigate this document Use the sidebar to jump to any section. Use ← → arrow keys for sequential navigation. The pagination bar shows your current position. The Track dropdown filters to DevOps or Developer content. Press Ctrl+P to print or save as PDF.

Platform Overview

Tech Stack

LayerTechnology
Backend servicesNestJS (TypeScript), Express
Frontend portalsNext.js 14 (App Router), Tailwind CSS
DatabaseSupabase (PostgreSQL 15) with Row Level Security
BlockchainPolygon (Solidity 0.8.24, Hardhat, ethers.js)
Queue / workersBullMQ + Redis
Email deliverySendGrid
File storageAWS S3 / Supabase Storage
PaymentsRazorpay
Certificate signingRSA-2048 + SHA-256
Monorepo toolingTurborepo, npm workspaces

Service Port Map

ServicePortType
api-gateway4000Express (reverse proxy + rate limiter)
auth-service4001NestJS
credential-service4002NestJS
verification-service4003NestJS
blockchain-service4004NestJS
notification-service4005NestJS
analytics-service4006NestJS
payment-service4007NestJS
admin-service4008NestJS
kyc-service4009NestJS
queue-worker4010NestJS (BullMQ worker, no HTTP)
crm-service4011NestJS
finance-service4012NestJS
marketplace-service4013NestJS
helpdesk-service4014NestJS
WEB PORTALS
issuer-portal3001Next.js 14 — Issuers manage certificates
recipient-wallet3002Next.js 14 — Recipients view credentials
verification-portal3003Next.js 14 — Public QR/ID verification
admin-portal3004Next.js 14 — Platform admin dashboard
ca-portal3005Next.js 14 — Certificate Authority tools
cs-portal3006Next.js 14 — Customer support agents
staff-portal3007Next.js 14 — FinVault staff operations
marketplace3008Next.js 14 — Integration marketplace

Quick Start

Get the full platform running locally in about 5 minutes.

Prerequisites

ToolMinimum versionNotes
Node.js20.xUse nvm or fnm to manage versions
npm10.xBundled with Node 20
Docker DesktopLatestRequired for Redis + Mailhog
Supabase accountFree tier sufficient for dev
Polygon RPC endpointAlchemy or QuickNode free tier

Setup Steps

bash
# 1. Clone the repository
git clone https://github.com/mail2ratnakar/finvault.git
cd finvault

# 2. Copy environment template and fill in values
cp .env.example .env
# Edit .env — minimum required values listed below

# 3. Install all workspace dependencies
npm install

# 4. Start infrastructure (Redis on :6379, Mailhog on :8025)
bash infra/scripts/start-dev.sh

# 5. Apply database migrations (first time only)
supabase link --project-ref YOUR_PROJECT_REF
supabase db push

# 6. Start all 15 services in development mode
npm run dev

# 7. Verify everything is running
curl http://localhost:4000/health

Minimum Required Environment Variables

VariableExampleNotes
SUPABASE_URLhttps://xxx.supabase.coFrom Supabase project settings
SUPABASE_SERVICE_KEYeyJ…Service role key (never expose to browser)
SUPABASE_ANON_KEYeyJ…Anon/public key (safe for frontend)
JWT_SECRETmy-dev-secret-32chars-minimum!!Must be ≥ 32 characters
INTERNAL_JWT_SECRETinternal-32chars-minimum-secret!!Must be ≥ 32 characters — cannot be empty
KMS_ENCRYPTION_KEYbase64-encoded-32-byte-key==32-byte key, base64-encoded
REDIS_URLredis://localhost:6379Local Redis via Docker
POLYGON_RPC_URLhttps://polygon-amoy.g.alchemy.com/v2/KEYAmoy testnet for dev

Run Smoke Tests

bash
# With all services running, run the 12-test smoke suite (~60s)
npx ts-node infra/scripts/smoke-tests.ts

# Expected: 12 / 12 tests passing
# Tests cover: register -> login -> KYC -> template -> batch -> approve
#              -> poll -> verify -> revoke -> verify-revoked -> payment -> health
Supabase migrations required Services will fail with foreign key constraint errors if migrations have not been applied. Run supabase db push before starting services for the first time. See the Migrations Reference page for the full migration list.
Smart contract must be deployed Blockchain features (certificate anchoring, on-chain verification) require the FinVaultRegistry contract deployed and POLYGON_CONTRACT_ADDRESS set in .env. See the Blockchain / Polygon page.

System Diagram

FinVault is a microservices monorepo. All client traffic enters through a single API Gateway. Services communicate internally via HTTP with the X-Internal-Key header and via BullMQ job queues for async work.

Clients
Issuer Portal :3001
Recipient Wallet :3002
Verification Portal :3003
Admin Portal :3004
Staff Portal :3007
CA Portal :3005
CS Portal :3006
External API consumers
Edge / Security
Cloudflare WAF ⚠ NOT CONFIGURED
HTTPS / TLS termination
API Gateway :4000 — Rate Limiting, JWT verification, Routing
Rate limiter (per-IP, per-org)
JWT validation
Request ID injection
Reverse proxy to services
Backend Services (NestJS / Express)
auth :4001
credential :4002
verification :4003
blockchain :4004
notification :4005
analytics :4006
payment :4007
admin :4008
kyc :4009
queue-worker :4010
crm :4011
finance :4012
marketplace :4013
helpdesk :4014
External Integrations
Supabase PostgreSQL (DB + RLS)
Redis (BullMQ queues)
Polygon Network (blockchain anchor)
AWS S3 / Supabase Storage (files)
SendGrid (email)
Razorpay (payments)
🔴
Cloudflare WAF not configured The README notes "Place Cloudflare WAF in front of API gateway in production." This has not been done. The API Gateway is directly internet-exposed in the current setup. Configure Cloudflare before go-live. See the Pre-Production Checklist.

Microservices Map

All 15 backend services. Each exposes GET /health returning {"status":"ok","version":"x.y.z"}.

ServicePortFrameworkResponsibilityKey Dependencies
api-gateway4000ExpressReverse proxy, rate limiting, JWT pass-through, request ID injectionRedis, all downstream services
auth-service4001NestJSRegistration, login, JWT issuance, refresh rotation, TOTP MFA, password resetSupabase, Redis, notification-service, crm-service
credential-service4002NestJSCertificate templates, batch upload (CSV), single issuance, cert lifecycle managementSupabase, S3, BullMQ, blockchain-service
verification-service4003NestJSPublic cert verification (by ID or QR), revocation, blockchain cross-checkSupabase, blockchain-service, @finvault/crypto
blockchain-service4004NestJSPolygon contract interaction — anchor cert hash, verify anchor, RPC failoverPolygon RPC, FinVaultRegistry.sol, ethers.js
notification-service4005NestJSEmail delivery via SendGrid, webhook delivery, notification templates, preferencesSendGrid, Supabase, BullMQ
analytics-service4006NestJSUsage event ingestion, dashboard aggregations, retention and funnel metricsSupabase, Redis
payment-service4007NestJSRazorpay order creation, payment verification, credit ledger, invoice managementRazorpay, Supabase
admin-service4008NestJSPlatform admin — org management, KYC review, user management, impersonation, CERT-In endpointsSupabase, all services (internal calls)
kyc-service4009NestJSKYC submission intake, document upload, MCA CIN validation, review queueSupabase, S3
queue-worker4010NestJSBullMQ worker — processes cert issuance jobs, email jobs, blockchain anchor jobs asynchronouslyRedis, credential-service, blockchain-service, notification-service
crm-service4011NestJSLead management, contact tracking, activity logs, campaign managementSupabase
finance-service4012NestJSDouble-entry journal, general ledger, trial balance, tax invoices (decimal.js arithmetic)Supabase
marketplace-service4013NestJSIntegration connectors, LMS plugins (Moodle, Canvas), third-party webhooksSupabase
helpdesk-service4014NestJSSupport tickets, SLA tracking, CSAT, knowledge baseSupabase

Web Portals

All 8 portals are Next.js 14 applications using the App Router. They live in the web/ directory of the monorepo.

PortalDev PortDirectoryPrimary UsersAuth Mechanism
issuer-portal3001web/issuer-portalIssuing organisations (admins, staff)JWT (issuer role)
recipient-wallet3002web/recipient-walletCertificate recipientsJWT (recipient role)
verification-portal3003web/verification-portalPublic / anyone verifying a certificateNo auth (public)
admin-portal3004web/admin-portalPlatform adminsJWT (platform_admin role)
ca-portal3005web/ca-portalCertificate Authority reviewersJWT (ca_admin role)
cs-portal3006web/cs-portalCustomer support agentsJWT (cs_agent role)
staff-portal3007web/staff-portalFinVault internal staff (ops, sales, onboarding)JWT (staff role, STAFF_JWT_SECRET)
marketplace3008web/marketplaceThird-party developers browsing integrationsJWT (issuer role) for private routes
Staff portal uses a separate JWT secret STAFF_JWT_SECRET signs tokens for staff portal users. The helpdesk-service AgentAuthGuard accepts both STAFF_JWT_SECRET (type: 'staff') and JWT_SECRET (type: 'cs_agent') tokens via a decode-then-verify pattern.

Data Flow

Flow 1 — Certificate Issuance

  1. Issuer uploads CSV via issuer-portal → POST /credentials/batches → credential-service stores batch record and raw CSV in S3.
  2. Admin approves batchPOST /credentials/batches/:id/approve → credential-service enqueues cert-issue BullMQ jobs (one per CSV row).
  3. queue-worker processes jobs → for each job: reads recipient data, generates RSA-2048 signed cert JSON (RFC 8785 canonical form), stores in DB and S3, calls blockchain-service to anchor hash.
  4. blockchain-service → calls FinVaultRegistry.sol anchorCertificate(certHash, issuerAddress) on Polygon → stores txHash and blockNumber in blockchain_records.
  5. notification-service → sends branded email to recipient with cert ID, QR code, and download link.

Flow 2 — Certificate Verification

  1. Verifier opens QR code or enters cert ID → verification-portal calls GET /verify/:certId.
  2. verification-service → fetches cert from DB, reconstructs canonical JSON, verifies RSA-2048 signature against issuer's public key in key registry.
  3. Blockchain cross-check → calls blockchain-service to confirm cert hash is anchored on Polygon and matches DB record.
  4. Revocation check → confirms cert is not in revocation list.
  5. Result returned{ valid: true, issuer, recipient, issuedAt, blockchainTx } or structured error.

Flow 3 — KYC Flow

  1. Organisation submits KYCPOST /kyc/submit with CIN, incorporation docs → kyc-service stores submission, uploads docs to S3.
  2. Admin review queue → staff-portal /kyc page shows all under_review submissions.
  3. Admin approves or rejectsPOST /admin/orgs/:id/approve-kyc or reject-kyc → updates organisations.kyc_status and kyc_submissions (with reviewer_id, reviewed_at, notes).
  4. Notification sent → notification-service emails org admin with outcome and next steps.

Internal Communications

HTTP — createInternalClient

All synchronous inter-service calls use the shared createInternalClient helper from @finvault/utils. It automatically attaches the X-Internal-Key auth header and retries 3× with exponential backoff on 5xx errors.

typescript
import { createInternalClient } from '@finvault/utils';

// Create a typed client for a target service
const blockchainClient = createInternalClient(
  'blockchain-service',
  process.env.BLOCKCHAIN_SERVICE_URL  // e.g. http://localhost:4004
);

// All requests automatically include X-Internal-Key header
const result = await blockchainClient.post('/internal/anchor', {
  certHash: '0xabc...',
  issuerAddress: '0x123...'
});

Every service validates the X-Internal-Key header on /internal/* routes. The key is set via INTERNAL_SERVICE_KEY env var and is shared across all services.

🔴
INTERNAL_JWT_SECRET cannot be empty A prior security vulnerability allowed an empty INTERNAL_JWT_SECRET to fall back to a hardcoded value. This has been patched. Setting this env var to an empty string now throws at startup. Must be ≥ 32 characters.

Async — BullMQ Queues

Queue NameProducerConsumerPurpose
cert-issuecredential-servicequeue-workerAsync certificate generation + signing
blockchain-anchorqueue-workerqueue-workerPolygon transaction submission
email-sendMultiple servicesnotification-serviceAsync email delivery via SendGrid
kyc-notifyadmin-servicenotification-serviceKYC outcome email

All queues use Redis (configured via REDIS_URL). Jobs are retried 3× on failure. Failed jobs move to the failed state and can be inspected via Bull Board (not yet configured — add to production runbook).

Retry Policy

Schema Overview

FinVault uses Supabase (PostgreSQL 15) with Row Level Security enforced on all tenant data tables. The schema spans 53 migrations covering the full platform lifecycle.

Entity Groups

GroupKey TablesPurpose
Coreorganisations, users, certificates, templates, batchesThe primary business entities
Authrefresh_tokens, totp_backup_codes, user_sessions, failed_login_attemptsAuthentication state and security
Blockchainblockchain_records, issuer_keysOn-chain anchor data and cryptographic key registry
Billingsubscriptions, credit_ledger, invoices, payment_ordersCredits, billing cycles, Razorpay state
Complianceconsent_records, data_subject_requests, audit_logs, certin_log_retention, certin_incident_reportsDPDP consent, CERT-In compliance, audit trail
Multitenancyorg_members, custom_domains, org_invites, sso_sessionsMulti-tenant isolation, team management, SSO
Supporthelpdesk_tickets, kb_articles, csat_responsesCustomer support infrastructure
Monitoringsystem_alerts, service_health_historyPlatform observability

RLS Isolation Pattern

Every tenant table has an org_id foreign key. RLS policies use this pattern:

sql
-- Standard RLS policy on tenant tables
CREATE POLICY "org_isolation" ON certificates
  FOR ALL USING (
    org_id IN (
      SELECT org_id FROM org_members
      WHERE user_id = auth.uid()
    )
  );

-- Superadmin bypass (platform_admin role)
CREATE POLICY "platform_admin_bypass" ON certificates
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM users
      WHERE id = auth.uid() AND role = 'platform_admin'
    )
  );

Key Relationships

Migrations Reference

53 migrations in supabase/migrations/. Apply in order using supabase db push.

🔴
Migrations 0021–0053 not applied to production No production Supabase project has been provisioned. All 53 migrations must be applied to a fresh project before going live. Run: supabase link --project-ref YOUR_REF && supabase db push

How to Apply

bash
# Install Supabase CLI
npm install -g supabase

# Link to your project
supabase link --project-ref YOUR_PROJECT_REF

# Apply all pending migrations
supabase db push

# Verify which migrations have been applied
supabase migration list

Migration Table

#FileDescriptionStatus
10001_core_tables.sqlCore tables: organisations, users, certificates, templates, batches, blockchain_recordsApplied
20002_billing_analytics.sqlBilling: subscriptions, credit_ledger, invoices, analytics_events, audit_logsApplied
30003_rls_policies.sqlRow Level Security policies for all core tablesApplied
40004_auth_tables.sqlAuth: refresh_tokens, totp_backup_codes, user_sessionsApplied
50005_digilocker.sqlDigiLocker consent and token storage (Phase 2)Applied
60006_monitoring.sqlSystem alerts and service health historyApplied
70007_security.sqlAPI keys table, failed_login_attempts, account lockout countersApplied
80008_helpdesk.sqlSupport tickets tableApplied
90009_compliance.sqlDPDP compliance: consent_records, data_subject_requests, legal docsApplied
100010_marketing_cms.sqlMarketing CMS tables for landing pagesApplied
110011_issuer_keys.sqlIssuer RSA key pairs — encrypted storage for private keysApplied
120012_auth_hardening.sqlSHA-256 hashed refresh tokens, device fingerprintingApplied
130013_pii_separation.sqlPII separation — recipient personal data in isolated tableApplied
140014_storage.sqlSupabase Storage bucket policies for certificates and KYC docsApplied
150015_digilocker_crypto.sqlDigiLocker encrypted token storage fieldsApplied
160016_consent.sqlConsent version tracking and withdrawal timestampsApplied
170017_breach.sqlData breach incident reporting tableApplied
180018_revocation_hash.sqlCertificate revocation hash list for constant-time lookupsApplied
190019_consent_user_email.sqlUser email in consent records for DPDP erasure requestsApplied
200020_legal_acceptance.sqlLegal document acceptance tracking (ToS, privacy policy)Applied
⚠ Migrations 0021–0053 are pending — not yet applied to any production project
210021_…Finance engine — journal entries, general ledger (double-entry)Pending
220022_…Analytics enrichment — funnel tables, retention cohortsPending
350035_marketplace.sqlMarketplace: connectors, integrations, LMS plugins registryPending
360036_compliance_engine.sqlCompliance engine tables — policy rules, compliance checksPending
370037_org_team_invites.sqlTeam invite tokens with expiryPending
380038_subscription_events.sqlSubscription lifecycle event logPending
390039_sso_session.sqlSSO session tokens for SAML/OAuth flowsPending
400040_user_management.sqlEnhanced user management — roles, permissions, team hierarchyPending
470047_issuer_key_algorithm.sqlAlgorithm field on issuer keys for multi-algo supportPending
480048_finance_rls.sqlRLS policies for finance tablesPending
500050_atomic_journal_entry.sqlPostgres RPC for atomic double-entry journal insertsPending
510051_certin_incident_reporting.sqlCERT-In CE2 — incident reporting tablePending
520052_role_mfa_requirements.sqlPer-role MFA enforcement rules tablePending
530053_certin_log_retention.sqlCERT-In CE1 — 180-day log retention controlsPending

Note: Migrations 0023–0034, 0041–0046, and 0049 are omitted from the table above for brevity. Run ls supabase/migrations/ to see the complete list.

RLS Policies

Row Level Security (RLS) is enabled on all tables containing tenant data. Supabase enforces policies at the database level — no data can leak between organisations even if application-level auth is bypassed.

RLS-Enabled Tables

All tables in the Core, Billing, Compliance, Multitenancy, and Support groups have RLS enabled. System tables (system_alerts, service_health_history) use admin-only policies.

Client Key vs Service Role Key

KeyEnv varRLS applied?Use in
Anon/Public keySUPABASE_ANON_KEYYes — full RLS enforcementFrontend portals only
Service Role keySUPABASE_SERVICE_KEYNo — bypasses RLSBackend services only — never expose to browser
🔴
Never expose the service role key to the browser The service role key bypasses all RLS. It must only be used in backend services. All Next.js frontend code must use the anon key.

Edge Cases

Environment Variables

All services read configuration from environment variables. Copy .env.example to .env and fill in all Required values before starting services.

🔴
INTERNAL_JWT_SECRET cannot be empty Setting INTERNAL_JWT_SECRET to an empty string or omitting it will cause all services to fail at startup. This fallback was removed as a security fix. Must be ≥ 32 characters.
🔴
KMS_ENCRYPTION_KEY format enforced Must be a 32-byte key encoded as base64. Generate with: openssl rand -base64 32

Authentication

VariableReq?ServicesDescriptionExample
JWT_SECRETRequiredauth, all guardsSigns user JWT access tokens. ≥ 32 chars.my-super-secret-jwt-key-32chars!!
INTERNAL_JWT_SECRETRequiredall servicesSigns internal service-to-service JWTs. ≥ 32 chars. Cannot be empty.internal-secret-32-chars-minimum!!
STAFF_JWT_SECRETRequiredstaff-portal, helpdeskSigns JWT tokens for staff portal users. Separate from main JWT_SECRET.staff-secret-32chars-minimum!!
INTERNAL_SERVICE_KEYRequiredall servicesX-Internal-Key header value for inter-service calls.random-internal-service-key-here

Database

VariableReq?ServicesDescriptionExample
SUPABASE_URLRequiredallSupabase project URLhttps://xxx.supabase.co
SUPABASE_SERVICE_KEYRequiredall backend servicesService role key — bypasses RLS. Never use in frontend.eyJhbGci…
SUPABASE_ANON_KEYRequiredall frontend portalsAnon/public key — RLS enforced. Safe for browser.eyJhbGci…
REDIS_URLRequiredapi-gateway, queue-worker, analyticsRedis connection string for BullMQ and rate limiting.redis://localhost:6379

Cryptography & Blockchain

VariableReq?DescriptionExample
KMS_ENCRYPTION_KEYRequired32-byte AES-256-GCM key for encrypting issuer private keys at rest. Generate: openssl rand -base64 32k3y+base64encoded32byteshere==
PLATFORM_WALLET_PRIVATE_KEYRequiredPolygon wallet private key for gas payments and contract calls. Required unless USE_KMS_SIGNER=true.0xabc123…
POLYGON_RPC_URLRequiredPrimary Polygon RPC endpoint (Alchemy, QuickNode, or Infura).https://polygon-amoy.g.alchemy.com/v2/KEY
POLYGON_RPC_FALLBACK_URLOptionalFallback RPC if primary fails. Recommended for production.https://rpc-amoy.polygon.technology
POLYGON_CONTRACT_ADDRESSRequiredDeployed FinVaultRegistry contract address on Polygon.0x1234…
POLYGON_NETWORKRequiredNetwork name: amoy (testnet) or polygon (mainnet).amoy
USE_KMS_SIGNEROptionalSet to true to use AWS KMS instead of raw private key. KMS signer is a stub — requires implementation.false

External Services

VariableReq?DescriptionExample
SENDGRID_API_KEYRequiredSendGrid API key for email deliverySG.xxx
SENDGRID_FROM_EMAILRequiredVerified sender email address in SendGridnoreply@finvault.in
RAZORPAY_KEY_IDRequiredRazorpay API key ID for payment ordersrzp_live_xxx
RAZORPAY_KEY_SECRETRequiredRazorpay API secret for payment verificationxxx
AWS_ACCESS_KEY_IDOptionalAWS credentials for S3 file storage. Use Supabase Storage as alternative.AKIA…
AWS_SECRET_ACCESS_KEYOptionalAWS secret keyxxx
AWS_S3_BUCKETOptionalS3 bucket name for certificate file storagefinvault-certs-prod

Service URLs (inter-service)

VariableDefault (local)Notes
AUTH_SERVICE_URLhttp://localhost:4001Used by api-gateway for auth proxy
CREDENTIAL_SERVICE_URLhttp://localhost:4002
BLOCKCHAIN_SERVICE_URLhttp://localhost:4004
NOTIFICATION_SERVICE_URLhttp://localhost:4005

Infrastructure Setup

🔴
No cloud infrastructure has been provisioned There is no AWS ECS cluster, no ECR registry, no ALB, no Terraform state, no RDS instance (Supabase is used instead). This is a greenfield production deployment. Estimated effort to provision a baseline cloud stack: 1–2 days.

What Exists (Local Dev Only)

ComponentStatusLocation
Redis✅ Local via Dockerdocker-compose.yml
Mailhog (email trap)✅ Local via Dockerdocker-compose.yml
Supabase DB✅ Cloud (dev project)Supabase dashboard
Polygon Amoy testnet✅ ConnectedVia RPC env var

What Needs to Be Provisioned for Production

ComponentStatusRecommended Option
Container hosting (15 services)Not provisionedAWS ECS Fargate + ALB, or Railway, or Render
Container registryNot provisionedAWS ECR, GitHub Container Registry, or Docker Hub
Managed RedisNot provisionedAWS ElastiCache, Upstash, or Redis Cloud
Production Supabase projectNot provisionedCreate new Supabase project, apply all 53 migrations
Cloudflare WAFNot configuredCloudflare Free tier + WAF rules in front of API Gateway
DNS / domainNot configuredPoint api.finvault.in → load balancer after provisioning
TLS / SSLNot configuredACM (AWS) or Cloudflare universal SSL
Monitoring / alertingNot configuredDatadog, Grafana Cloud, or AWS CloudWatch
Log aggregationNot configuredAWS CloudWatch Logs, Datadog, or Logtail
CI/CD pipelineNot configuredGitHub Actions with ECR push + ECS deploy

Minimum Server Requirements (per service)

ResourceMinimumRecommended
CPU0.25 vCPU0.5 vCPU
Memory256 MB512 MB
Node.js20.x20.x LTS

With 15 services, total minimum RAM is ~3.75 GB. Recommended total: ~7.5 GB across the fleet. The queue-worker and blockchain-service may spike higher under load.

Docker & Redis

Local development uses Docker Compose to run Redis and Mailhog. No application services run in Docker — they run directly via npm run dev.

Start Infrastructure

bash
# Start Redis (:6379) and Mailhog (:8025 web, :1025 SMTP)
bash infra/scripts/start-dev.sh

# Verify Redis is running
redis-cli ping  # Expected: PONG

# View Mailhog inbox (captured emails in local dev)
open http://localhost:8025

docker-compose.yml Services

ServiceImagePortsPurpose
redisredis:7-alpine6379BullMQ queues + rate limiter state + session cache
mailhogmailhog/mailhog1025 (SMTP), 8025 (Web UI)Local email trap — captures all outgoing emails

Redis Configuration

Redis is used for three purposes:

Set REDIS_URL=redis://localhost:6379 for local dev. For production, use a managed Redis service (Upstash, ElastiCache, Redis Cloud).

Supabase Setup

FinVault uses Supabase (managed PostgreSQL 15) for the database, file storage, and authentication infrastructure.

🔴
Apply all 53 migrations before starting services Services will fail with foreign key constraint errors if migrations have not been applied. A fresh Supabase project has no tables.

Initial Setup

bash
# Install Supabase CLI
npm install -g supabase

# Log in
supabase login

# Link to your project (get ref from Supabase dashboard -> Settings -> General)
supabase link --project-ref YOUR_PROJECT_REF

# Apply all 53 migrations in order
supabase db push

# Confirm which migrations are applied
supabase migration list

Storage Buckets

The following storage buckets must exist (created by migrations, but verify):

BucketAccessPurpose
certificatesPrivate (RLS)Issued certificate PDF/JSON files
kyc-docsPrivate (RLS)KYC uploaded documents
templatesPrivate (RLS)Certificate template assets

Key Settings to Configure in Supabase Dashboard

Blockchain / Polygon

FinVault anchors certificate hashes on the Polygon network using the FinVaultRegistry Solidity smart contract.

🔴
CertiK audit not completed — do not deploy to Polygon mainnet The smart contract has not been audited. Do not deploy to Polygon mainnet until a CertiK (or equivalent) security audit has been completed and all findings resolved. Testnet (Amoy) deployment is fine for testing.

Contract: FinVaultRegistry.sol

Located at contracts/FinVaultRegistry.sol. Key functions:

Contract uses OpenZeppelin Ownable with two-step ownership transfer for security.

Deploy to Amoy Testnet

bash
cd contracts

# Install Hardhat dependencies
npm install

# Deploy to Amoy testnet
npx hardhat deploy --network amoy

# Contract address is printed after deployment -- save to .env
# POLYGON_CONTRACT_ADDRESS=0x...

# Verify on Polygonscan (optional but recommended)
npx hardhat verify --network amoy CONTRACT_ADDRESS

Environment Variables

VariableValue
POLYGON_NETWORKamoy for testnet, polygon for mainnet
POLYGON_CONTRACT_ADDRESSAddress printed after hardhat deploy
POLYGON_RPC_URLPrimary RPC (Alchemy/QuickNode recommended)
POLYGON_RPC_FALLBACK_URLFallback RPC for automatic failover
PLATFORM_WALLET_PRIVATE_KEYPolygon wallet with MATIC for gas
Fund the platform wallet before production deploy The platform wallet (PLATFORM_WALLET_PRIVATE_KEY) pays gas for every anchorCertificate transaction. Ensure it has sufficient MATIC (Polygon mainnet) before enabling blockchain features in production.

Production Runbook

Step-by-step sequence for the first production deployment. Complete the Pre-Production Checklist before starting.

Go-Live Sequence

  1. 1
    Provision cloud infrastructure — ECS cluster + task definitions, ECR repositories, ALB with target groups, security groups, VPC. Estimated: 4–8h.
  2. 2
    Provision managed Redis — Upstash (simplest) or ElastiCache. Note the Redis URL for env vars.
  3. 3
    Create production Supabase project — New project in Supabase dashboard. Enable connection pooling (PgBouncer, transaction mode).
  4. 4
    Set all environment variables — In ECS task definitions (or Railway/Render env settings). Reference the Environment Variables page.
  5. 5
    Apply database migrationssupabase link --project-ref PROD_REF && supabase db push. Verify all 53 migrations applied: supabase migration list.
  6. 6
    Deploy smart contract — After CertiK audit completion only. npx hardhat deploy --network polygon. Fund platform wallet with MATIC first. Update POLYGON_CONTRACT_ADDRESS.
  7. 7
    Build and push Docker imagesnpm run build → build Dockerfile per service → push to ECR. Tag with git SHA.
  8. 8
    Deploy all 15 services — Update ECS task definitions with new image tags. Force new deployment on each service. Verify health endpoints respond 200.
  9. 9
    Deploy all 8 web portals — Build Next.js apps (npm run build) and deploy to Vercel, Cloudflare Pages, or serve static from S3+CloudFront.
  10. 10
    Run smoke tests against productionAPI_BASE=https://api.finvault.in npx ts-node infra/scripts/smoke-tests.ts. Expect 12/12 passing.
  11. 11
    Configure Cloudflare WAF — Point api.finvault.in DNS → ALB through Cloudflare. Enable WAF rules: rate limiting, bot protection, OWASP core rule set.
  12. 12
    Configure email domain — Add SendGrid DMARC/SPF/DKIM DNS records for the finvault.in sending domain to prevent email spoofing.
  13. 13
    Enable monitoring — Configure health check alarms, error rate alerts, and latency dashboards. Set up on-call rotation.
  14. 14
    Complete go-live sign-off — Fill in the Pre-Production Checklist with owners and sign-off dates.

Health Check Endpoints

All 15 services expose GET /health returning {"status":"ok","version":"x.y.z","service":"name"}. Use these for load balancer health checks and monitoring.

Rollback Procedure

  1. Identify the previous good Docker image tag (git SHA of last known-good commit)
  2. Update ECS task definition to previous image tag for affected service(s)
  3. Force new deployment: aws ecs update-service --force-new-deployment
  4. Database migrations are additive — no DB rollback is needed for service rollbacks
  5. If a migration must be rolled back, it requires a new forward migration (never delete applied migrations)

Authentication API

Base URL: http://localhost:4001 (proxied through API Gateway at localhost:4000/auth)
Auth: Most endpoints are unauthenticated. GET /auth/me requires Authorization: Bearer <token>.

JWT token lifetimes Access tokens expire in 15 minutes. Refresh tokens expire in 7 days. On each /auth/refresh call, a new refresh token is issued and the old one is invalidated (rotation).
POST /auth/register Register a new organisation and admin user
json
{
  "org_name": "Acme University",
  "slug": "acme-university",
  "domain": "https://acme.edu",
  "email": "admin@acme.edu",
  "full_name": "Jane Smith",
  "password": "S3cur3P@ss"
}
json
{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci...",
  "user": { "id": "uuid", "email": "admin@acme.edu", "role": "issuer" },
  "org": { "id": "uuid", "name": "Acme University", "slug": "acme-university" }
}
StatusCodeDescription
400VALIDATION_ERRORMissing or invalid fields
409CONFLICTEmail or org slug already exists
POST /auth/login Login with email and password
json
{ "email": "admin@acme.edu", "password": "S3cur3P@ss" }
json
{
  "access_token": "eyJhbGci...",
  "refresh_token": "eyJhbGci...",
  "user": { "id": "uuid", "email": "admin@acme.edu", "role": "issuer" }
}
json
{ "totp_required": true, "totp_session_token": "tmp_token_..." }
StatusDescription
401Invalid credentials
423Account locked (too many failed attempts)
POST /auth/refresh Refresh access token (rotation — old refresh token invalidated)
json
{ "refresh_token": "eyJhbGci..." }
json
{ "access_token": "eyJhbGci...", "refresh_token": "new_token..." }
POST /auth/totp/enroll Begin TOTP MFA enrollment — returns QR code URI

Requires: Authorization: Bearer <token>

json
{
  "secret": "BASE32SECRET",
  "otpauth_url": "otpauth://totp/FinVault:user@email.com?secret=...",
  "backup_codes": ["abc123", "def456", "..."]
}
POST /auth/totp/verify Verify TOTP code after password login (completes MFA login)
json
{ "totp_session_token": "tmp_token_...", "code": "123456" }
json
{ "access_token": "eyJhbGci...", "refresh_token": "eyJhbGci..." }
GET /auth/me Get current authenticated user and organisation

Requires: Authorization: Bearer <token>

json
{
  "id": "uuid",
  "email": "admin@acme.edu",
  "full_name": "Jane Smith",
  "role": "issuer",
  "totp_enrolled": true,
  "org": { "id": "uuid", "name": "Acme University", "kyc_status": "approved" }
}
POST /auth/logout Invalidate refresh token and end session
json
{ "refresh_token": "eyJhbGci..." }
json
{ "message": "Logged out" }

Credential API

Base URL: http://localhost:4002 (proxied via API Gateway)
Auth: All endpoints require Authorization: Bearer <token> or X-API-Key: fvk_live_...

GET /credentials List certificates for authenticated organisation (paginated)
ParamTypeDefaultDescription
statusstringpending | issued | revoked
searchstringSearch by recipient name or email
pageinteger1Page number
limitinteger20Max 100
json
{
  "data": [{ "id": "cert_uuid", "status": "issued", "recipient_email": "...", "issued_at": "..." }],
  "total": 150, "page": 1, "limit": 20
}
POST /credentials/issue Issue a single certificate immediately
json
{
  "template_id": "template_uuid",
  "recipient": {
    "name": "John Doe",
    "email": "john@example.com",
    "metadata": { "course": "Data Science", "grade": "A" }
  }
}
json
{ "id": "cert_uuid", "status": "pending", "job_id": "bull_job_id" }
StatusDescription
402Insufficient credits — top up at POST /payments/orders
403KYC not approved — organisation must complete KYC first
POST /credentials/batches Upload a CSV batch for bulk issuance

Content-Type: multipart/form-data

FieldTypeDescription
fileCSV fileMax 10MB. Columns: name, email, metadata fields matching template
template_idstringUUID of the certificate template to use
json
{ "batch_id": "batch_uuid", "status": "pending_approval", "row_count": 250 }
POST /credentials/batches/:batchId/approve Approve a pending batch — triggers issuance jobs

Requires admin or issuer role. Enqueues one BullMQ cert-issue job per CSV row.

json
{ "batch_id": "batch_uuid", "status": "processing", "jobs_enqueued": 250 }
GET /credentials/batches/:batchId Poll batch processing status
json
{
  "batch_id": "batch_uuid",
  "status": "processing",
  "total": 250,
  "issued": 180,
  "failed": 2,
  "pending": 68
}

Verification API

Base URL: http://localhost:4003 (proxied via API Gateway)
Auth: GET /verify/:certId is public (no auth required). Revocation requires issuer auth.

GET /verify/:certId Verify a certificate by ID (public endpoint)
json
{
  "valid": true,
  "cert_id": "cert_uuid",
  "recipient_name": "John Doe",
  "issued_at": "2026-01-15T10:30:00Z",
  "issuer": { "name": "Acme University", "verified": true },
  "blockchain": { "tx_hash": "0xabc...", "block_number": 12345678, "anchored_at": "2026-01-15T10:31:00Z" },
  "revoked": false
}
json
{ "valid": false, "revoked": true, "revoked_at": "2026-03-01T09:00:00Z", "reason": "Issued in error" }
StatusDescription
404Certificate not found
422Signature verification failed — cert data may be tampered
POST /verify/bulk Verify multiple certificates in one request
json
{ "cert_ids": ["cert_uuid_1", "cert_uuid_2"] }
json
{ "results": [{ "cert_id": "...", "valid": true }, { "cert_id": "...", "valid": false }] }
DELETE /verify/:certId/revoke Revoke a certificate (issuer auth required)

Requires: Authorization: Bearer <issuer_token>. Only the issuing organisation can revoke.

json
{ "reason": "Issued in error — recipient data incorrect" }
json
{ "cert_id": "cert_uuid", "revoked": true, "revoked_at": "2026-06-03T12:00:00Z" }

Blockchain API

Base URL: http://localhost:4004 — internal service only. Not proxied through the public API Gateway. Called by credential-service and queue-worker via X-Internal-Key.
Auth: X-Internal-Key: <INTERNAL_SERVICE_KEY> header required on all endpoints.

POST /internal/anchor Anchor a certificate hash on Polygon
json
{ "cert_id": "cert_uuid", "cert_hash": "0xabc...", "issuer_address": "0x123..." }
json
{ "tx_hash": "0xdef...", "block_number": 12345678, "anchored_at": "2026-06-03T12:00:00Z" }
GET /internal/verify/:certHash Check if a cert hash is anchored on-chain
json
{ "anchored": true, "issuer_address": "0x123...", "timestamp": 1717416000, "block_number": 12345678 }
GET /health Blockchain service health — includes RPC connectivity check
json
{
  "status": "ok", "version": "1.1.0",
  "blockchain": { "connected": true, "network": "amoy", "block": 12345678 }
}

KYC API

Base URL: http://localhost:4009 (proxied via API Gateway)
Auth: Issuer endpoints require issuer JWT. Admin review endpoints require platform_admin JWT.

POST /kyc/submit Submit KYC application with company details
json
{
  "cin": "U72200MH2020PTC123456",
  "company_name": "Acme University",
  "registered_address": "123 Main St, Mumbai",
  "contact_name": "Jane Smith",
  "contact_email": "kyc@acme.edu",
  "contact_phone": "+91-9876543210"
}
json
{ "submission_id": "kyc_uuid", "status": "under_review" }
POST /kyc/documents Upload KYC supporting documents (multipart)

Content-Type: multipart/form-data

FieldDescription
filePDF or image. Max 10MB per file.
document_typecoi (Certificate of Incorporation) | pan | gst | address_proof
json
{ "document_id": "doc_uuid", "document_type": "coi", "url": "https://storage/..." }
GET /kyc/status Get KYC status for the authenticated organisation
json
{ "kyc_status": "approved", "submitted_at": "2026-01-10T09:00:00Z", "reviewed_at": "2026-01-12T11:00:00Z" }
GET /admin/kyc/queue List all KYC submissions under review (admin only)

Requires: platform_admin role. Returns paginated list of under_review submissions.

json
{ "data": [{ "submission_id": "...", "org_name": "...", "cin": "...", "submitted_at": "..." }], "total": 12 }

Payments API

Base URL: http://localhost:4007 (proxied via API Gateway)
Auth: All endpoints require Authorization: Bearer <token>

FinVault uses a credit pack model. Organisations buy credit packs (50 / 250 / 750 / 2000 certificate credits) via Razorpay. Each certificate issuance deducts 1 credit.

POST /payments/orders Create a Razorpay order for a credit pack purchase
json
{ "pack": "250" }

Valid pack values: "50" (₹499), "250" (₹1,999), "750" (₹4,999), "2000" (₹9,999)

json
{
  "order_id": "order_rzp_...",
  "razorpay_order_id": "order_rzp_...",
  "amount": 199900,
  "currency": "INR",
  "key_id": "rzp_live_..."
}
POST /payments/verify Verify Razorpay payment and credit the account
json
{
  "razorpay_order_id": "order_rzp_...",
  "razorpay_payment_id": "pay_rzp_...",
  "razorpay_signature": "hmac_sha256_signature"
}
json
{ "credits_added": 250, "new_balance": 312 }
GET /payments/balance Get current credit balance for the organisation
json
{ "balance": 312, "lifetime_purchased": 500, "lifetime_used": 188 }
GET /payments/invoices List payment invoices for the organisation
json
{ "data": [{ "id": "inv_uuid", "amount": 199900, "credits": 250, "paid_at": "2026-05-01T10:00:00Z" }] }

Admin API

Base URL: http://localhost:4008 (proxied via API Gateway)
Auth: All endpoints require Authorization: Bearer <platform_admin_token> unless noted.

Organisation Management

GET /admin/orgs List all organisations (paginated, filterable)
ParamDescription
searchSearch by org name or domain
tierstarter | growth | enterprise
kyc_statusnot_submitted | under_review | approved | rejected
statusactive | suspended | blocked
page, limitPagination
POST /admin/orgs/:id/approve-kyc Approve KYC for an organisation
json
{ "org_id": "uuid", "kyc_status": "approved", "reviewed_by": "admin_uuid", "reviewed_at": "2026-06-03T12:00:00Z" }
POST /admin/orgs/:id/reject-kyc Reject KYC with required reason
json
{ "reason": "Provided CIN does not match company name in MCA records" }
POST /admin/orgs/:id/suspend Suspend an organisation (blocks login and API access)
json
{ "reason": "Policy violation — suspicious issuance pattern" }

CERT-In Compliance Endpoints

POST /admin/certin/incident CERT-In CE2 — Report a cybersecurity incident
json
{
  "incident_type": "data_breach",
  "severity": "high",
  "description": "Unauthorised access attempt detected on admin portal",
  "detected_at": "2026-06-03T08:00:00Z",
  "affected_systems": ["admin-portal", "admin-service"]
}
GET /admin/certin/poc-contact CERT-In CE7 — Get Point of Contact information
json
{ "name": "FinVault Security Team", "email": "security@finvault.in", "phone": "+91-XXX-XXX-XXXX" }

Analytics API

Base URL: http://localhost:4006
Auth: All endpoints require Authorization: Bearer <token> (issuer or admin role).

POST/analytics/events Ingest a usage event
json
{ "event_type": "cert_issued", "payload": { "cert_id": "uuid", "template_id": "uuid" } }
json
{ "event_id": "uuid", "recorded_at": "2026-06-03T12:00:00Z" }
GET/analytics/dashboard Get dashboard summary stats for the organisation
json
{
  "certs_issued_total": 1250,
  "certs_issued_this_month": 180,
  "verifications_this_month": 430,
  "credit_balance": 312,
  "active_templates": 5
}

Marketplace API

Base URL: http://localhost:4013
Auth: Read endpoints are public. Write endpoints require issuer JWT.

GET/marketplace/connectors List all available integration connectors
json
{ "data": [{ "id": "moodle", "name": "Moodle LMS", "type": "lms", "active": true }] }
POST/marketplace/connectors/:id/activate Activate a connector for the organisation
json
{ "config": { "moodle_url": "https://moodle.acme.edu", "api_token": "moodle_token" } }

Helpdesk API

Base URL: http://localhost:4014
Auth: Issuer endpoints require issuer JWT. Agent endpoints require CS agent or staff JWT (dual-token via AgentAuthGuard).

Dual-token authentication on agent routes /helpdesk/agent/* routes accept both staff portal tokens (signed with STAFF_JWT_SECRET) and CS agent tokens (signed with JWT_SECRET). The guard decodes the token to read type, then verifies with the correct secret.
POST/helpdesk/tickets Create a support ticket
json
{ "subject": "Certificate not received", "description": "Batch 123 shows issued but recipient did not get email", "priority": "medium" }
json
{ "ticket_id": "TKT-0042", "status": "open", "sla_deadline": "2026-06-05T12:00:00Z" }
GET/helpdesk/tickets List tickets for the organisation (or all tickets for agents)
ParamDescription
statusopen | in_progress | resolved | closed
prioritylow | medium | high | urgent

Notifications API

Base URL: http://localhost:4005
Auth: Internal endpoints use X-Internal-Key. Preference endpoints use Bearer JWT.

POST/internal/notify Send an email notification (internal service call)
json
{
  "template": "cert_issued",
  "to": "john@example.com",
  "variables": { "recipient_name": "John Doe", "cert_id": "cert_uuid", "verify_url": "https://verify.finvault.in/cert_uuid" }
}
GET/notifications/preferences Get notification preferences for authenticated user
json
{ "cert_issued": true, "batch_complete": true, "kyc_update": true, "payment_receipt": true }

Webhooks Reference

FinVault sends webhook events to your registered endpoint URL for key platform events. Configure your webhook endpoint URL in the issuer portal settings.

Signature Verification

Every webhook delivery includes an X-FinVault-Signature header. Verify it before processing:

typescript
import * as crypto from 'crypto';

function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  if (signature.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express handler example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-finvault-signature'] as string;
  if (!verifyWebhookSignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // process event...
  res.sendStatus(200);
});

Delivery Policy

Event Catalog

EVENT cert.issued Fired when a certificate is successfully issued and blockchain-anchored
json
{
  "event": "cert.issued", "timestamp": "2026-06-03T12:00:00Z",
  "data": {
    "cert_id": "cert_uuid", "recipient_email": "john@example.com",
    "recipient_name": "John Doe", "template_id": "template_uuid",
    "blockchain_tx": "0xabc...", "verify_url": "https://verify.finvault.in/cert_uuid"
  }
}
EVENT cert.revoked Fired when a certificate is revoked
json
{ "event": "cert.revoked", "timestamp": "2026-06-03T12:00:00Z", "data": { "cert_id": "cert_uuid", "reason": "Issued in error", "revoked_by": "issuer_uuid" } }
EVENT kyc.approved Fired when an organisation's KYC is approved
json
{ "event": "kyc.approved", "timestamp": "2026-06-03T12:00:00Z", "data": { "org_id": "uuid", "org_name": "Acme University" } }
EVENT payment.success Fired when a credit pack payment is verified
json
{ "event": "payment.success", "timestamp": "2026-06-03T12:00:00Z", "data": { "org_id": "uuid", "credits_added": 250, "amount_inr": 1999 } }
EVENT batch.approved Fired when a batch is approved and issuance jobs are enqueued
json
{ "event": "batch.approved", "timestamp": "2026-06-03T12:00:00Z", "data": { "batch_id": "uuid", "row_count": 250, "template_id": "uuid" } }

Repo Structure

FinVault is a Turborepo monorepo with npm workspaces. All packages are TypeScript.

text
finvault/
├── apps/                        # Backend microservices
│   ├── admin-service/           # :4008 -- Platform admin
│   ├── analytics-service/       # :4006 -- Usage analytics
│   ├── api-gateway/             # :4000 -- Reverse proxy + rate limiter
│   ├── auth-service/            # :4001 -- Authentication + JWT
│   ├── blockchain-service/      # :4004 -- Polygon interaction
│   ├── credential-service/      # :4002 -- Certificate management
│   ├── crm-service/             # :4011 -- Lead management
│   ├── finance-service/         # :4012 -- Double-entry ledger
│   ├── helpdesk-service/        # :4014 -- Support tickets
│   ├── kyc-service/             # :4009 -- KYC submission
│   ├── marketplace-service/     # :4013 -- Integration connectors
│   ├── notification-service/    # :4005 -- Email + webhook delivery
│   ├── payment-service/         # :4007 -- Razorpay integration
│   ├── queue-worker/            # :4010 -- BullMQ job processor
│   └── verification-service/    # :4003 -- Public cert verification
│
├── web/                         # Frontend portals (Next.js 14)
│   ├── admin-portal/            # :3004 -- Platform admin dashboard
│   ├── ca-portal/               # :3005 -- Certificate Authority
│   ├── cs-portal/               # :3006 -- Customer support
│   ├── issuer-portal/           # :3001 -- Issuing organisations
│   ├── marketplace/             # :3008 -- Integration marketplace
│   ├── marketing-site/          # :3000 -- Public landing page
│   ├── recipient-wallet/        # :3002 -- Recipient credentials
│   ├── staff-portal/            # :3007 -- Internal staff ops
│   └── verification-portal/     # :3003 -- Public verification
│
├── packages/                    # Shared TypeScript packages
│   ├── abuse/                   # Rate limiting, lockout helpers
│   ├── config/                  # Env validation schemas
│   ├── crypto/                  # RSA, AES-256-GCM, canonical JSON
│   ├── db/                      # Supabase client factory
│   ├── lms-plugins/             # LMS integration adapters
│   ├── types/                   # Shared TypeScript types
│   ├── ui/                      # Shared React components
│   └── utils/                   # createInternalClient, logger, etc.
│
├── contracts/                   # Solidity smart contracts
│   ├── FinVaultRegistry.sol     # Main certificate registry contract
│   └── hardhat.config.ts        # Hardhat + Polygon network config
│
├── supabase/
│   └── migrations/              # 53 SQL migration files (apply in order)
│
├── infra/
│   ├── docker/                  # Dockerfiles per service
│   ├── scripts/                 # start-dev.sh, smoke-tests.ts, validate-env.ts
│   └── test-fixtures/           # test-batch.csv for smoke tests
│
├── docs/
│   ├── openapi/                 # finvault-api.yaml, webhooks.yaml
│   ├── superpowers/             # Design specs and implementation plans
│   ├── CHANGELOG.md
│   ├── BUILD_INFO.md
│   ├── SECURITY_CHECKLIST.md
│   └── finvault-handover.html   # ← This document
│
├── turbo.json                   # Turborepo pipeline config
├── package.json                 # Workspace root -- npm scripts
└── tsconfig.base.json           # Shared TypeScript config

Key npm Scripts (run from repo root)

CommandDescription
npm run devStart all services in watch mode (Turborepo parallel)
npm run buildBuild all packages and services
npm run testRun all Jest test suites
npm run type-checkTypeScript type check across all packages
npm run lintESLint across all packages
turbo run dev --filter=auth-serviceRun only auth-service in dev mode

Adding a New Service

Follow these steps to add a new NestJS microservice to the FinVault monorepo.

  1. Scaffold the app:
    bash
    cd apps
    mkdir my-service && cd my-service
    npm init -y
    # Standard NestJS directory structure:
    mkdir -p src/my-module
    # Create: src/main.ts, src/app.module.ts, src/my-module/my-module.module.ts
    # Create: src/my-module/my-module.controller.ts, src/my-module/my-module.service.ts
  2. Add to turbo.json pipeline: Add "apps/my-service" to the workspaces array in package.json root.
  3. Assign a port — next available after :4014. Update README.md service port map.
  4. Add to docker-compose.yml for local dev (optional, services run natively in dev mode).
  5. Implement health endpoint:
    typescript
    // src/health/health.controller.ts
    import { Controller, Get } from '@nestjs/common';
    @Controller()
    export class HealthController {
      @Get('health')
      health() {
        return { status: 'ok', version: process.env.npm_package_version, service: 'my-service' };
      }
    }
  6. Register in api-gateway — add proxy route in apps/api-gateway/src/index.ts pointing to the new service.
  7. Add env validation:
    typescript
    // src/config/env.validation.ts
    import { IsString, IsNotEmpty, validateSync } from 'class-validator';
    import { plainToInstance } from 'class-transformer';
    
    class EnvironmentVariables {
      @IsString() @IsNotEmpty() MY_REQUIRED_VAR: string;
    }
    
    export function validate(config: Record<string, unknown>) {
      const validated = plainToInstance(EnvironmentVariables, config, { enableImplicitConversion: true });
      const errors = validateSync(validated);
      if (errors.length > 0) throw new Error(errors.toString());
      return validated;
    }
  8. Add to smoke tests — extend infra/scripts/smoke-tests.ts with a health check for the new service.

Shared Packages

All shared code lives in packages/ and is imported as @finvault/<name>.

@finvault/types

Shared TypeScript interfaces and enums: Certificate, Organisation, User, Batch, Template, CertStatus, KycStatus, OrgTier.

@finvault/crypto

All cryptographic operations. Never implement crypto outside this package.

typescript
import { signCertificate, verifyCertificate, canonicalJson, timingSafeEqual, encryptWithAES, decryptWithAES } from '@finvault/crypto';

// RSA-2048 sign cert JSON
const signature = await signCertificate(certJson, privateKeyPem);

// AES-256-GCM encrypt a private key for storage
const encrypted = encryptWithAES(privateKeyPem, process.env.KMS_ENCRYPTION_KEY);

@finvault/utils

Inter-service communication and request utilities.

typescript
import { createInternalClient, requestIdMiddleware, logger } from '@finvault/utils';

// HTTP client with automatic X-Internal-Key header and 3x retry
const client = createInternalClient('blockchain-service', process.env.BLOCKCHAIN_SERVICE_URL);
const result = await client.post('/internal/anchor', body);

@finvault/db

Supabase client factory. Always use this — never instantiate createClient directly in services.

typescript
import { getSupabaseAdmin, getSupabaseClient } from '@finvault/db';

// Service role client (bypasses RLS) -- use in backend services only
const adminClient = getSupabaseAdmin();

// Anon client (RLS enforced) -- use with user JWT for user-scoped queries
const userClient = getSupabaseClient(userJwt);

@finvault/abuse

Rate limiting and account security helpers: checkRateLimit, recordFailedLogin, checkAccountLocked, resetFailedLogins.

@finvault/config

Environment variable validation schemas for each service. Extend BaseEnvSchema for service-specific vars.

Testing Strategy

Test Layers

LayerToolLocationScope
Unit testsJestapps/*/src/**/*.spec.tsIndividual service methods, DTOs, guards. Uses mocked dependencies.
Integration testsJest + real Supabaseapps/*/src/**/*.integration.spec.tsDB queries, RLS policy checks. Requires test Supabase project.
Smoke teststs-node scriptinfra/scripts/smoke-tests.ts12-test end-to-end flow against running services.

Current Test Status

355 / 363 tests passing 8 tests are currently failing. See docs/ISSUES.md for details. Known failures are in Phase 1 gap-closure features that were implemented without TDD due to git not being initialised at the time.

Running Tests

bash
# Run all tests across all services
npm run test

# Run tests for a single service
turbo run test --filter=auth-service

# Run tests in watch mode
turbo run test --filter=auth-service -- --watch

# Run smoke tests (requires all services running)
npx ts-node infra/scripts/smoke-tests.ts

# Run with coverage
turbo run test --filter=credential-service -- --coverage

Writing Tests for a New Endpoint

typescript
// apps/my-service/src/my-module/my-module.service.spec.ts
import { Test } from '@nestjs/testing';
import { MyModuleService } from './my-module.service';
import { getSupabaseAdmin } from '@finvault/db';

jest.mock('@finvault/db');

describe('MyModuleService', () => {
  let service: MyModuleService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [MyModuleService],
    }).compile();
    service = module.get(MyModuleService);
  });

  it('should create a record', async () => {
    (getSupabaseAdmin as jest.Mock).mockReturnValue({
      from: () => ({ insert: () => ({ select: () => ({ single: () => ({ data: { id: 'uuid' }, error: null }) }) }) })
    });
    const result = await service.create({ name: 'test' });
    expect(result.id).toBe('uuid');
  });
});

Coding Patterns

NestJS Module Structure

text
src/
├── main.ts                      # Bootstrap + global pipes + CORS
├── app.module.ts                # Root module -- imports all feature modules
└── certs/
    ├── certs.module.ts          # Feature module -- providers, imports
    ├── certs.controller.ts      # HTTP route handlers
    ├── certs.service.ts         # Business logic
    ├── dto/
    │   ├── create-cert.dto.ts   # class-validator decorated DTOs
    │   └── update-cert.dto.ts
    └── certs.service.spec.ts    # Unit tests co-located with service

DTO Validation Pattern

Always use class-validator decorators. The global ValidationPipe in main.ts handles rejection automatically.

typescript
import { IsString, IsEmail, IsNotEmpty, MaxLength } from 'class-validator';

export class IssueCertificateDto {
  @IsString() @IsNotEmpty() template_id: string;
  @IsEmail() recipient_email: string;
  @IsString() @IsNotEmpty() @MaxLength(200) recipient_name: string;
}

// In main.ts -- already configured, do not remove:
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));

Guard Composition

typescript
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';

@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('issuer')
@Post('/issue')
async issue(@Body() dto: IssueCertificateDto, @Req() req) {
  return this.certsService.issue(req.user.orgId, dto);
}
🔴
Always pin JWT algorithm to HS256 Never call jwt.verify(token, secret) without specifying { algorithms: ['HS256'] }. Omitting this allows algorithm confusion attacks (HS256 → RS256 swap). This was a critical security finding fixed in the repair run.

Monetary Arithmetic

typescript
import Decimal from 'decimal.js';

// Correct -- use decimal.js for all monetary values
const total = new Decimal(price).times(quantity).toFixed(2);

// Wrong -- native float arithmetic causes precision errors
// const total = price * quantity; // Never use for INR amounts

Error Response Format

All services return errors in this format:

json
{
  "statusCode": 400,
  "message": "Validation failed: email must be a valid email address",
  "error": "VALIDATION_ERROR",
  "requestId": "req_abc123"
}

Audit Logging

Write to audit_logs on all state-changing admin operations:

typescript
await getSupabaseAdmin()
  .from('audit_logs')
  .insert({
    action: 'kyc.approved',
    actor_id: req.user.id,
    target_type: 'organisation',
    target_id: orgId,
    metadata: { reviewer_notes: notes },
    org_id: orgId,
  });

Security Architecture

JWT Token Security

Cryptography

OperationAlgorithmImplementation
Certificate signingRSA-2048 + SHA-256@finvault/cryptosignCertificate()
Certificate verificationRSA-2048 + SHA-256@finvault/cryptoverifyCertificate()
Private key encryption at restAES-256-GCM@finvault/cryptoencryptWithAES()
Canonical JSON (hash input)RFC 8785@finvault/cryptocanonicalJson()
Timing-safe comparisonHMAC-based@finvault/cryptotimingSafeEqual()
TOTP backup codesbcrypt (rounds=12)Stored hashed, single-use
Refresh tokens (storage)SHA-256Never store raw token

API Key Scopes

API keys (prefix fvk_live_) have explicit scopes: read, write, admin. Each endpoint declares its required scope. Keys without the required scope get 403.

Rate Limiting

Endpoint groupLimit
Auth endpoints (/auth/*)20 req/min per IP
Verification (/verify/*)200 req/min per IP
Admin (/admin/*)30 req/min per IP
General API100 req/min per IP

Account Lockout

After 5 consecutive failed login attempts, the account is locked for 15 minutes. Tracked in failed_login_attempts table via @finvault/abuse.

CERT-In Compliance

FinVault implements CERT-In (Indian Computer Emergency Response Team) cybersecurity directives.

RequirementDescriptionImplementationStatus
CE1 180-day log retention Migration 0053_certin_log_retention.sqlcertin_log_retention table with retention policies ✅ Done
CE2 Cybersecurity incident reporting Migration 0051_certin_incident_reporting.sql + POST /admin/certin/incident endpoint ✅ Done
CE5 Multi-factor authentication TOTP MFA (Google Authenticator) on all admin and issuer accounts ✅ Done
CE7 Point of Contact information GET /admin/certin/poc-contact endpoint returns security contact details ✅ Done
CE3, CE4, CE6 Additional directives Not yet implemented — assess applicability and implement before go-live ⚠ Pending Review
CE3/CE4/CE6 not assessed CE1, CE2, CE5, CE7 have been implemented. CE3 (synchronising ICT system clocks), CE4 (designated PoC for law enforcement), and CE6 (audit trail maintenance) have not been formally assessed. Review these before production go-live.

OWASP Hardening

The 2026-06-03 security repair run addressed all OWASP Top 10 findings. See docs/SECURITY_CHECKLIST.md and docs/PATCHES.md for full details.

OWASPCategoryFindingFixStatus
A01Broken Access ControlAnalytics service routes unauthenticated; CA portal IDORAdded auth guards to all routes; fixed IDOR with org_id ownership check
A02Cryptographic FailuresPrivate keys not encrypted at rest in all pathsAES-256-GCM encryption via @finvault/crypto on all key storage
A03InjectionMissing global ValidationPipe; ruleData field injectionGlobal ValidationPipe added to all services; field whitelist on ruleData
A04Insecure DesignTOTP secret fetched before password validationFetch TOTP secret only after password is verified
A05Security MisconfigurationCORS wildcard; rate limiter JWT claim key bug; duplicate admin limiterCORS restricted to ALLOWED_ORIGINS; JWT claim key fixed; duplicate removed
A07Authentication FailuresJWT algorithm confusion; account lockout missing; timing-unsafe compareHS256 pinned on all guards; account lockout via @finvault/abuse; timingSafeEqual
A08Software/Data IntegrityNon-canonical JSON in cert hash; no blockchain cross-checkRFC 8785 canonical JSON; blockchain anchor verified on every verification
A09Logging FailuresNo request ID tracing; gaps in audit log coverageRequest ID injected at gateway; propagated via X-Request-ID header; audit_logs table
A10SSRFPDF background image URL not validated — could fetch internal metadata endpointsURL allowlist for external domains only; internal IP ranges blocked
A06Vulnerable Components17 npm vulnerabilities in auditNot yet resolved — see Pre-Production Checklist⚠ Pending

Responsible Disclosure

Reporting a Vulnerability

🔴
Do not open a public GitHub issue for security vulnerabilities Email security@finvault.in with subject [SECURITY] <brief description>. We will acknowledge within 48 hours and triage within 7 days.

Response SLAs

SeverityPatch Target
Critical7 days
High30 days
Medium90 days
LowNext release

Out of Scope

Coordination

We follow responsible disclosure: allow us to remediate before public disclosure. We will coordinate a disclosure timeline with you after a fix is available. See .well-known/security.txt (served from api.finvault.in/.well-known/security.txt) for the machine-readable security policy.

Pre-Production Checklist

Complete all 🔴 Critical items before any production traffic. Complete 🟡 High items before end of first week. 🟢 Done items are listed for confidence.

🔴 Critical — Block Go-Live

🔴
1. Apply Supabase migrations 0021–0053 to production project
No production Supabase project exists. Create one and run supabase db push. All 53 migrations must be applied in order before services start.
Critical Effort: 2h Owner: TBD → Supabase Setup
🔴
2. Provision cloud infrastructure
No ECS cluster, ECR registry, ALB, Terraform state, or managed Redis has been provisioned. Greenfield deployment required. Choose: AWS ECS Fargate, Railway, or Render.
Critical Effort: 1–2 days Owner: TBD → Infrastructure Setup
🔴
3. Complete CertiK smart contract audit before Polygon mainnet deploy
FinVaultRegistry.sol has not been audited. Do not deploy to Polygon mainnet until a security audit is complete and all findings resolved. Amoy testnet is fine for testing.
Critical Effort: 2–4 weeks Owner: TBD → Blockchain Setup
🔴
4. Wire KMS signer to real AWS KMS or GCP KMS
The current KMS signer is a stub (USE_KMS_SIGNER=false). In production, the platform wallet private key must be stored in a hardware-backed KMS, not as a raw env var.
Critical Effort: 1 day Owner: TBD
🔴
5. Resolve 17 npm security vulnerabilities
Run npm audit to see current findings. Upgrade or patch affected packages before production. At minimum, resolve all Critical and High severity vulns.
Critical Effort: 4–8h Owner: TBD
🔴
6. Configure Cloudflare WAF in front of API Gateway
The API Gateway is currently internet-exposed with no WAF protection. Configure Cloudflare with WAF rules, rate limiting, and bot protection before accepting production traffic.
Critical Effort: 2h Owner: TBD

🟡 High — Fix Before First Users

🟡
7. Configure DMARC/SPF/DKIM for SendGrid sending domain
Add DNS records for finvault.in sending domain in SendGrid. Without these, emails may be flagged as spam or rejected by recipient mail servers.
HighEffort: 1h
🟡
8. Update .env.example with all new env vars from repair run
Several new env vars were added during the security repair run (INTERNAL_JWT_SECRET, STAFF_JWT_SECRET, etc.) that may not be in .env.example. Audit and update.
HighEffort: 2h
🟡
9. Fix 8 failing tests
8 of 363 tests currently fail (mostly Phase 1 gap-closure features implemented without TDD). Fix before production to ensure CI gate is clean. See docs/ISSUES.md.
HighEffort: 4–8h
🟡
10. Verify and run supabase db push — resolve migration 0031 duplicate
A known duplicate exists in migration 0031. Verify the migration file and resolve before running supabase db push on the production project.
HighEffort: 2h
🟡
11. Set up CI/CD pipeline (GitHub Actions → ECR → ECS)
No automated deployment pipeline exists. Build, test, and deploy must be automated before team velocity can be maintained post-launch.
HighEffort: 1 day

🟢 Done — Security Hardening Complete

32 security checklist items resolved (2026-06-03 repair run)
All items in docs/SECURITY_CHECKLIST.md — S001–S028, AUTH-C1/C2, FIN-C1/C2, CRED-C1, CHAIN-C1, PAY-C1, ADMIN-C1, HELP-C1, MKT-C1/C2 — resolved across 40 commits.
355 / 363 tests passingCERT-In CE1/CE2/CE5/CE7 ✅OWASP A01–A05, A07–A10 ✅

Go-Live Sign-Off

#ItemOwnerSign-off DateNotes
1Migrations applied to production Supabase
2Cloud infrastructure provisioned
3Smart contract audited + deployed to mainnet
4KMS signer wired
5npm audit clean (Critical/High)
6Cloudflare WAF configured
7Email DNS records (DMARC/SPF/DKIM) set
8All tests passing (363 / 363)
9Smoke tests pass against production URL
10Monitoring and alerting configured