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:
- DevOps / Engineering Lead track — infrastructure setup, environment variables, deployment runbook, go-live sequence. Pages 1–17 + 39.
- Developer Deep-Dive track — architecture rationale, API contracts, data model, coding patterns, how to extend the platform. Pages 1–11, 18–38.
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
| Field | Value |
|---|---|
| Version | v1.1.0 |
| Git commit | 4d626e6 (2026-06-03 security-hardened release) |
| Node.js | 20.x |
| Polygon network | Amoy testnet (mainnet pending CertiK audit) |
| GitHub | github.com/mail2ratnakar/finvault |
Phase Status
| Phase | Description | Status |
|---|---|---|
| Phase 1 — MVP | Core issuance, blockchain anchoring, public verification, KYC, billing | ✅ Complete |
| Phase 2 — Pre-launch | Recipient wallet, DigiLocker, advanced analytics, helpdesk, multitenancy | 🔄 In Progress |
| Phase 3 — Scale | SDK, white-label, bulk API, enterprise SLAs, SSO/SAML | ⏳ Not Started |
Platform Overview
Tech Stack
| Layer | Technology |
|---|---|
| Backend services | NestJS (TypeScript), Express |
| Frontend portals | Next.js 14 (App Router), Tailwind CSS |
| Database | Supabase (PostgreSQL 15) with Row Level Security |
| Blockchain | Polygon (Solidity 0.8.24, Hardhat, ethers.js) |
| Queue / workers | BullMQ + Redis |
| Email delivery | SendGrid |
| File storage | AWS S3 / Supabase Storage |
| Payments | Razorpay |
| Certificate signing | RSA-2048 + SHA-256 |
| Monorepo tooling | Turborepo, npm workspaces |
Service Port Map
| Service | Port | Type |
|---|---|---|
| api-gateway | 4000 | Express (reverse proxy + rate limiter) |
| auth-service | 4001 | NestJS |
| credential-service | 4002 | NestJS |
| verification-service | 4003 | NestJS |
| blockchain-service | 4004 | NestJS |
| notification-service | 4005 | NestJS |
| analytics-service | 4006 | NestJS |
| payment-service | 4007 | NestJS |
| admin-service | 4008 | NestJS |
| kyc-service | 4009 | NestJS |
| queue-worker | 4010 | NestJS (BullMQ worker, no HTTP) |
| crm-service | 4011 | NestJS |
| finance-service | 4012 | NestJS |
| marketplace-service | 4013 | NestJS |
| helpdesk-service | 4014 | NestJS |
| WEB PORTALS | ||
| issuer-portal | 3001 | Next.js 14 — Issuers manage certificates |
| recipient-wallet | 3002 | Next.js 14 — Recipients view credentials |
| verification-portal | 3003 | Next.js 14 — Public QR/ID verification |
| admin-portal | 3004 | Next.js 14 — Platform admin dashboard |
| ca-portal | 3005 | Next.js 14 — Certificate Authority tools |
| cs-portal | 3006 | Next.js 14 — Customer support agents |
| staff-portal | 3007 | Next.js 14 — FinVault staff operations |
| marketplace | 3008 | Next.js 14 — Integration marketplace |
Quick Start
Get the full platform running locally in about 5 minutes.
Prerequisites
| Tool | Minimum version | Notes |
|---|---|---|
| Node.js | 20.x | Use nvm or fnm to manage versions |
| npm | 10.x | Bundled with Node 20 |
| Docker Desktop | Latest | Required for Redis + Mailhog |
| Supabase account | — | Free tier sufficient for dev |
| Polygon RPC endpoint | — | Alchemy or QuickNode free tier |
Setup Steps
# 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
| Variable | Example | Notes |
|---|---|---|
SUPABASE_URL | https://xxx.supabase.co | From Supabase project settings |
SUPABASE_SERVICE_KEY | eyJ… | Service role key (never expose to browser) |
SUPABASE_ANON_KEY | eyJ… | Anon/public key (safe for frontend) |
JWT_SECRET | my-dev-secret-32chars-minimum!! | Must be ≥ 32 characters |
INTERNAL_JWT_SECRET | internal-32chars-minimum-secret!! | Must be ≥ 32 characters — cannot be empty |
KMS_ENCRYPTION_KEY | base64-encoded-32-byte-key== | 32-byte key, base64-encoded |
REDIS_URL | redis://localhost:6379 | Local Redis via Docker |
POLYGON_RPC_URL | https://polygon-amoy.g.alchemy.com/v2/KEY | Amoy testnet for dev |
Run Smoke Tests
# 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 db push before starting services for the first time. See the Migrations Reference page for the full migration list.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.
Microservices Map
All 15 backend services. Each exposes GET /health returning {"status":"ok","version":"x.y.z"}.
| Service | Port | Framework | Responsibility | Key Dependencies |
|---|---|---|---|---|
| api-gateway | 4000 | Express | Reverse proxy, rate limiting, JWT pass-through, request ID injection | Redis, all downstream services |
| auth-service | 4001 | NestJS | Registration, login, JWT issuance, refresh rotation, TOTP MFA, password reset | Supabase, Redis, notification-service, crm-service |
| credential-service | 4002 | NestJS | Certificate templates, batch upload (CSV), single issuance, cert lifecycle management | Supabase, S3, BullMQ, blockchain-service |
| verification-service | 4003 | NestJS | Public cert verification (by ID or QR), revocation, blockchain cross-check | Supabase, blockchain-service, @finvault/crypto |
| blockchain-service | 4004 | NestJS | Polygon contract interaction — anchor cert hash, verify anchor, RPC failover | Polygon RPC, FinVaultRegistry.sol, ethers.js |
| notification-service | 4005 | NestJS | Email delivery via SendGrid, webhook delivery, notification templates, preferences | SendGrid, Supabase, BullMQ |
| analytics-service | 4006 | NestJS | Usage event ingestion, dashboard aggregations, retention and funnel metrics | Supabase, Redis |
| payment-service | 4007 | NestJS | Razorpay order creation, payment verification, credit ledger, invoice management | Razorpay, Supabase |
| admin-service | 4008 | NestJS | Platform admin — org management, KYC review, user management, impersonation, CERT-In endpoints | Supabase, all services (internal calls) |
| kyc-service | 4009 | NestJS | KYC submission intake, document upload, MCA CIN validation, review queue | Supabase, S3 |
| queue-worker | 4010 | NestJS | BullMQ worker — processes cert issuance jobs, email jobs, blockchain anchor jobs asynchronously | Redis, credential-service, blockchain-service, notification-service |
| crm-service | 4011 | NestJS | Lead management, contact tracking, activity logs, campaign management | Supabase |
| finance-service | 4012 | NestJS | Double-entry journal, general ledger, trial balance, tax invoices (decimal.js arithmetic) | Supabase |
| marketplace-service | 4013 | NestJS | Integration connectors, LMS plugins (Moodle, Canvas), third-party webhooks | Supabase |
| helpdesk-service | 4014 | NestJS | Support tickets, SLA tracking, CSAT, knowledge base | Supabase |
Web Portals
All 8 portals are Next.js 14 applications using the App Router. They live in the web/ directory of the monorepo.
| Portal | Dev Port | Directory | Primary Users | Auth Mechanism |
|---|---|---|---|---|
| issuer-portal | 3001 | web/issuer-portal | Issuing organisations (admins, staff) | JWT (issuer role) |
| recipient-wallet | 3002 | web/recipient-wallet | Certificate recipients | JWT (recipient role) |
| verification-portal | 3003 | web/verification-portal | Public / anyone verifying a certificate | No auth (public) |
| admin-portal | 3004 | web/admin-portal | Platform admins | JWT (platform_admin role) |
| ca-portal | 3005 | web/ca-portal | Certificate Authority reviewers | JWT (ca_admin role) |
| cs-portal | 3006 | web/cs-portal | Customer support agents | JWT (cs_agent role) |
| staff-portal | 3007 | web/staff-portal | FinVault internal staff (ops, sales, onboarding) | JWT (staff role, STAFF_JWT_SECRET) |
| marketplace | 3008 | web/marketplace | Third-party developers browsing integrations | JWT (issuer role) for private routes |
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
- Issuer uploads CSV via issuer-portal →
POST /credentials/batches→ credential-service stores batch record and raw CSV in S3. - Admin approves batch →
POST /credentials/batches/:id/approve→ credential-service enqueuescert-issueBullMQ jobs (one per CSV row). - 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.
- blockchain-service → calls
FinVaultRegistry.solanchorCertificate(certHash, issuerAddress)on Polygon → storestxHashandblockNumberinblockchain_records. - notification-service → sends branded email to recipient with cert ID, QR code, and download link.
Flow 2 — Certificate Verification
- Verifier opens QR code or enters cert ID → verification-portal calls
GET /verify/:certId. - verification-service → fetches cert from DB, reconstructs canonical JSON, verifies RSA-2048 signature against issuer's public key in key registry.
- Blockchain cross-check → calls blockchain-service to confirm cert hash is anchored on Polygon and matches DB record.
- Revocation check → confirms cert is not in revocation list.
- Result returned →
{ valid: true, issuer, recipient, issuedAt, blockchainTx }or structured error.
Flow 3 — KYC Flow
- Organisation submits KYC →
POST /kyc/submitwith CIN, incorporation docs → kyc-service stores submission, uploads docs to S3. - Admin review queue → staff-portal
/kycpage shows allunder_reviewsubmissions. - Admin approves or rejects →
POST /admin/orgs/:id/approve-kycorreject-kyc→ updatesorganisations.kyc_statusandkyc_submissions(with reviewer_id, reviewed_at, notes). - 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.
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 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 Name | Producer | Consumer | Purpose |
|---|---|---|---|
cert-issue | credential-service | queue-worker | Async certificate generation + signing |
blockchain-anchor | queue-worker | queue-worker | Polygon transaction submission |
email-send | Multiple services | notification-service | Async email delivery via SendGrid |
kyc-notify | admin-service | notification-service | KYC 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
- HTTP retries: 3 attempts, exponential backoff starting at 250ms
- Retry on: 5xx errors, network timeouts
- No retry on: 4xx errors (client errors are not transient)
- BullMQ job retries: 3 attempts, configurable delay per queue
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
| Group | Key Tables | Purpose |
|---|---|---|
| Core | organisations, users, certificates, templates, batches | The primary business entities |
| Auth | refresh_tokens, totp_backup_codes, user_sessions, failed_login_attempts | Authentication state and security |
| Blockchain | blockchain_records, issuer_keys | On-chain anchor data and cryptographic key registry |
| Billing | subscriptions, credit_ledger, invoices, payment_orders | Credits, billing cycles, Razorpay state |
| Compliance | consent_records, data_subject_requests, audit_logs, certin_log_retention, certin_incident_reports | DPDP consent, CERT-In compliance, audit trail |
| Multitenancy | org_members, custom_domains, org_invites, sso_sessions | Multi-tenant isolation, team management, SSO |
| Support | helpdesk_tickets, kb_articles, csat_responses | Customer support infrastructure |
| Monitoring | system_alerts, service_health_history | Platform observability |
RLS Isolation Pattern
Every tenant table has an org_id foreign key. RLS policies use this pattern:
-- 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
certificates→batches→templates→organisationscertificates→blockchain_records(one-to-one, nullable until anchored)users→org_members→organisations(many-to-many, with role)credit_ledger→organisations(credit balance tracked via ledger entries)audit_logs→organisations,users(immutable append-only)
Migrations Reference
53 migrations in supabase/migrations/. Apply in order using supabase db push.
supabase link --project-ref YOUR_REF && supabase db pushHow to Apply
# 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
| # | File | Description | Status |
|---|---|---|---|
10001_core_tables.sql | Core tables: organisations, users, certificates, templates, batches, blockchain_records | Applied | |
20002_billing_analytics.sql | Billing: subscriptions, credit_ledger, invoices, analytics_events, audit_logs | Applied | |
30003_rls_policies.sql | Row Level Security policies for all core tables | Applied | |
40004_auth_tables.sql | Auth: refresh_tokens, totp_backup_codes, user_sessions | Applied | |
50005_digilocker.sql | DigiLocker consent and token storage (Phase 2) | Applied | |
60006_monitoring.sql | System alerts and service health history | Applied | |
70007_security.sql | API keys table, failed_login_attempts, account lockout counters | Applied | |
80008_helpdesk.sql | Support tickets table | Applied | |
90009_compliance.sql | DPDP compliance: consent_records, data_subject_requests, legal docs | Applied | |
100010_marketing_cms.sql | Marketing CMS tables for landing pages | Applied | |
110011_issuer_keys.sql | Issuer RSA key pairs — encrypted storage for private keys | Applied | |
120012_auth_hardening.sql | SHA-256 hashed refresh tokens, device fingerprinting | Applied | |
130013_pii_separation.sql | PII separation — recipient personal data in isolated table | Applied | |
140014_storage.sql | Supabase Storage bucket policies for certificates and KYC docs | Applied | |
150015_digilocker_crypto.sql | DigiLocker encrypted token storage fields | Applied | |
160016_consent.sql | Consent version tracking and withdrawal timestamps | Applied | |
170017_breach.sql | Data breach incident reporting table | Applied | |
180018_revocation_hash.sql | Certificate revocation hash list for constant-time lookups | Applied | |
190019_consent_user_email.sql | User email in consent records for DPDP erasure requests | Applied | |
200020_legal_acceptance.sql | Legal 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 cohorts | Pending | |
350035_marketplace.sql | Marketplace: connectors, integrations, LMS plugins registry | Pending | |
360036_compliance_engine.sql | Compliance engine tables — policy rules, compliance checks | Pending | |
370037_org_team_invites.sql | Team invite tokens with expiry | Pending | |
380038_subscription_events.sql | Subscription lifecycle event log | Pending | |
390039_sso_session.sql | SSO session tokens for SAML/OAuth flows | Pending | |
400040_user_management.sql | Enhanced user management — roles, permissions, team hierarchy | Pending | |
470047_issuer_key_algorithm.sql | Algorithm field on issuer keys for multi-algo support | Pending | |
480048_finance_rls.sql | RLS policies for finance tables | Pending | |
500050_atomic_journal_entry.sql | Postgres RPC for atomic double-entry journal inserts | Pending | |
510051_certin_incident_reporting.sql | CERT-In CE2 — incident reporting table | Pending | |
520052_role_mfa_requirements.sql | Per-role MFA enforcement rules table | Pending | |
530053_certin_log_retention.sql | CERT-In CE1 — 180-day log retention controls | Pending | |
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
| Key | Env var | RLS applied? | Use in |
|---|---|---|---|
| Anon/Public key | SUPABASE_ANON_KEY | Yes — full RLS enforcement | Frontend portals only |
| Service Role key | SUPABASE_SERVICE_KEY | No — bypasses RLS | Backend services only — never expose to browser |
Edge Cases
- Public verification —
certificateshas a separatepublic_verificationpolicy that allows unauthenticatedSELECTon the cert ID and status fields only. Full cert data remains org-isolated. - Platform admin bypass — Users with
role = 'platform_admin'in theuserstable bypass org_id isolation policies for admin operations. - Audit logs —
audit_logsis append-only. NoUPDATEorDELETEpolicies exist — logs are immutable by design.
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 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.openssl rand -base64 32Authentication
| Variable | Req? | Services | Description | Example |
|---|---|---|---|---|
| JWT_SECRET | Required | auth, all guards | Signs user JWT access tokens. ≥ 32 chars. | my-super-secret-jwt-key-32chars!! |
| INTERNAL_JWT_SECRET | Required | all services | Signs internal service-to-service JWTs. ≥ 32 chars. Cannot be empty. | internal-secret-32-chars-minimum!! |
| STAFF_JWT_SECRET | Required | staff-portal, helpdesk | Signs JWT tokens for staff portal users. Separate from main JWT_SECRET. | staff-secret-32chars-minimum!! |
| INTERNAL_SERVICE_KEY | Required | all services | X-Internal-Key header value for inter-service calls. | random-internal-service-key-here |
Database
| Variable | Req? | Services | Description | Example |
|---|---|---|---|---|
| SUPABASE_URL | Required | all | Supabase project URL | https://xxx.supabase.co |
| SUPABASE_SERVICE_KEY | Required | all backend services | Service role key — bypasses RLS. Never use in frontend. | eyJhbGci… |
| SUPABASE_ANON_KEY | Required | all frontend portals | Anon/public key — RLS enforced. Safe for browser. | eyJhbGci… |
| REDIS_URL | Required | api-gateway, queue-worker, analytics | Redis connection string for BullMQ and rate limiting. | redis://localhost:6379 |
Cryptography & Blockchain
| Variable | Req? | Description | Example |
|---|---|---|---|
| KMS_ENCRYPTION_KEY | Required | 32-byte AES-256-GCM key for encrypting issuer private keys at rest. Generate: openssl rand -base64 32 | k3y+base64encoded32byteshere== |
| PLATFORM_WALLET_PRIVATE_KEY | Required | Polygon wallet private key for gas payments and contract calls. Required unless USE_KMS_SIGNER=true. | 0xabc123… |
| POLYGON_RPC_URL | Required | Primary Polygon RPC endpoint (Alchemy, QuickNode, or Infura). | https://polygon-amoy.g.alchemy.com/v2/KEY |
| POLYGON_RPC_FALLBACK_URL | Optional | Fallback RPC if primary fails. Recommended for production. | https://rpc-amoy.polygon.technology |
| POLYGON_CONTRACT_ADDRESS | Required | Deployed FinVaultRegistry contract address on Polygon. | 0x1234… |
| POLYGON_NETWORK | Required | Network name: amoy (testnet) or polygon (mainnet). | amoy |
| USE_KMS_SIGNER | Optional | Set to true to use AWS KMS instead of raw private key. KMS signer is a stub — requires implementation. | false |
External Services
| Variable | Req? | Description | Example |
|---|---|---|---|
| SENDGRID_API_KEY | Required | SendGrid API key for email delivery | SG.xxx |
| SENDGRID_FROM_EMAIL | Required | Verified sender email address in SendGrid | noreply@finvault.in |
| RAZORPAY_KEY_ID | Required | Razorpay API key ID for payment orders | rzp_live_xxx |
| RAZORPAY_KEY_SECRET | Required | Razorpay API secret for payment verification | xxx |
| AWS_ACCESS_KEY_ID | Optional | AWS credentials for S3 file storage. Use Supabase Storage as alternative. | AKIA… |
| AWS_SECRET_ACCESS_KEY | Optional | AWS secret key | xxx |
| AWS_S3_BUCKET | Optional | S3 bucket name for certificate file storage | finvault-certs-prod |
Service URLs (inter-service)
| Variable | Default (local) | Notes |
|---|---|---|
| AUTH_SERVICE_URL | http://localhost:4001 | Used by api-gateway for auth proxy |
| CREDENTIAL_SERVICE_URL | http://localhost:4002 | |
| BLOCKCHAIN_SERVICE_URL | http://localhost:4004 | |
| NOTIFICATION_SERVICE_URL | http://localhost:4005 |
Infrastructure Setup
What Exists (Local Dev Only)
| Component | Status | Location |
|---|---|---|
| Redis | ✅ Local via Docker | docker-compose.yml |
| Mailhog (email trap) | ✅ Local via Docker | docker-compose.yml |
| Supabase DB | ✅ Cloud (dev project) | Supabase dashboard |
| Polygon Amoy testnet | ✅ Connected | Via RPC env var |
What Needs to Be Provisioned for Production
| Component | Status | Recommended Option |
|---|---|---|
| Container hosting (15 services) | Not provisioned | AWS ECS Fargate + ALB, or Railway, or Render |
| Container registry | Not provisioned | AWS ECR, GitHub Container Registry, or Docker Hub |
| Managed Redis | Not provisioned | AWS ElastiCache, Upstash, or Redis Cloud |
| Production Supabase project | Not provisioned | Create new Supabase project, apply all 53 migrations |
| Cloudflare WAF | Not configured | Cloudflare Free tier + WAF rules in front of API Gateway |
| DNS / domain | Not configured | Point api.finvault.in → load balancer after provisioning |
| TLS / SSL | Not configured | ACM (AWS) or Cloudflare universal SSL |
| Monitoring / alerting | Not configured | Datadog, Grafana Cloud, or AWS CloudWatch |
| Log aggregation | Not configured | AWS CloudWatch Logs, Datadog, or Logtail |
| CI/CD pipeline | Not configured | GitHub Actions with ECR push + ECS deploy |
Minimum Server Requirements (per service)
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 0.25 vCPU | 0.5 vCPU |
| Memory | 256 MB | 512 MB |
| Node.js | 20.x | 20.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
# 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
| Service | Image | Ports | Purpose |
|---|---|---|---|
| redis | redis:7-alpine | 6379 | BullMQ queues + rate limiter state + session cache |
| mailhog | mailhog/mailhog | 1025 (SMTP), 8025 (Web UI) | Local email trap — captures all outgoing emails |
Redis Configuration
Redis is used for three purposes:
- BullMQ queues —
queue-workerprocesses all async jobs. Queue names:cert-issue,blockchain-anchor,email-send,kyc-notify - Rate limiting state —
api-gatewaytracks per-IP and per-org request counts - Session caching — optional session data for fast auth lookups
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.
Initial Setup
# 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):
| Bucket | Access | Purpose |
|---|---|---|
certificates | Private (RLS) | Issued certificate PDF/JSON files |
kyc-docs | Private (RLS) | KYC uploaded documents |
templates | Private (RLS) | Certificate template assets |
Key Settings to Configure in Supabase Dashboard
- Email auth → disable (FinVault uses its own auth-service, not Supabase Auth)
- JWT expiry → not relevant (FinVault issues its own JWTs)
- Connection pooling → enable PgBouncer in transaction mode for production (prevents connection exhaustion with 15 services)
- Point-in-time recovery → enable for production data protection
Blockchain / Polygon
FinVault anchors certificate hashes on the Polygon network using the FinVaultRegistry Solidity smart contract.
Contract: FinVaultRegistry.sol
Located at contracts/FinVaultRegistry.sol. Key functions:
anchorCertificate(bytes32 certHash, address issuerAddress)— records cert hash + issuer on-chain. EmitsCertificateAnchoredevent.verifyCertificate(bytes32 certHash)— returns anchor data (issuerAddress, timestamp, blockNumber).revokeCertificate(bytes32 certHash)— marks cert as revoked on-chain. Only callable by original issuer.
Contract uses OpenZeppelin Ownable with two-step ownership transfer for security.
Deploy to Amoy Testnet
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
| Variable | Value |
|---|---|
| POLYGON_NETWORK | amoy for testnet, polygon for mainnet |
| POLYGON_CONTRACT_ADDRESS | Address printed after hardhat deploy |
| POLYGON_RPC_URL | Primary RPC (Alchemy/QuickNode recommended) |
| POLYGON_RPC_FALLBACK_URL | Fallback RPC for automatic failover |
| PLATFORM_WALLET_PRIVATE_KEY | Polygon wallet with MATIC for gas |
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
- 1Provision cloud infrastructure — ECS cluster + task definitions, ECR repositories, ALB with target groups, security groups, VPC. Estimated: 4–8h.
- 2Provision managed Redis — Upstash (simplest) or ElastiCache. Note the Redis URL for env vars.
- 3Create production Supabase project — New project in Supabase dashboard. Enable connection pooling (PgBouncer, transaction mode).
- 4Set all environment variables — In ECS task definitions (or Railway/Render env settings). Reference the Environment Variables page.
- 5Apply database migrations —
supabase link --project-ref PROD_REF && supabase db push. Verify all 53 migrations applied:supabase migration list. - 6Deploy smart contract — After CertiK audit completion only.
npx hardhat deploy --network polygon. Fund platform wallet with MATIC first. UpdatePOLYGON_CONTRACT_ADDRESS. - 7Build and push Docker images —
npm run build→ build Dockerfile per service → push to ECR. Tag with git SHA. - 8Deploy all 15 services — Update ECS task definitions with new image tags. Force new deployment on each service. Verify health endpoints respond
200. - 9Deploy all 8 web portals — Build Next.js apps (
npm run build) and deploy to Vercel, Cloudflare Pages, or serve static from S3+CloudFront. - 10Run smoke tests against production —
API_BASE=https://api.finvault.in npx ts-node infra/scripts/smoke-tests.ts. Expect 12/12 passing. - 11Configure Cloudflare WAF — Point
api.finvault.inDNS → ALB through Cloudflare. Enable WAF rules: rate limiting, bot protection, OWASP core rule set. - 12Configure email domain — Add SendGrid DMARC/SPF/DKIM DNS records for the
finvault.insending domain to prevent email spoofing. - 13Enable monitoring — Configure health check alarms, error rate alerts, and latency dashboards. Set up on-call rotation.
- 14Complete 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
- Identify the previous good Docker image tag (git SHA of last known-good commit)
- Update ECS task definition to previous image tag for affected service(s)
- Force new deployment:
aws ecs update-service --force-new-deployment - Database migrations are additive — no DB rollback is needed for service rollbacks
- 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>.
/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 ▶
{
"org_name": "Acme University",
"slug": "acme-university",
"domain": "https://acme.edu",
"email": "admin@acme.edu",
"full_name": "Jane Smith",
"password": "S3cur3P@ss"
}{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"user": { "id": "uuid", "email": "admin@acme.edu", "role": "issuer" },
"org": { "id": "uuid", "name": "Acme University", "slug": "acme-university" }
}| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid fields |
| 409 | CONFLICT | Email or org slug already exists |
POST /auth/login Login with email and password ▶
{ "email": "admin@acme.edu", "password": "S3cur3P@ss" }{
"access_token": "eyJhbGci...",
"refresh_token": "eyJhbGci...",
"user": { "id": "uuid", "email": "admin@acme.edu", "role": "issuer" }
}{ "totp_required": true, "totp_session_token": "tmp_token_..." }| Status | Description |
|---|---|
| 401 | Invalid credentials |
| 423 | Account locked (too many failed attempts) |
POST /auth/refresh Refresh access token (rotation — old refresh token invalidated) ▶
{ "refresh_token": "eyJhbGci..." }{ "access_token": "eyJhbGci...", "refresh_token": "new_token..." }POST /auth/totp/enroll Begin TOTP MFA enrollment — returns QR code URI ▶
Requires: Authorization: Bearer <token>
{
"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) ▶
{ "totp_session_token": "tmp_token_...", "code": "123456" }{ "access_token": "eyJhbGci...", "refresh_token": "eyJhbGci..." }GET /auth/me Get current authenticated user and organisation ▶
Requires: Authorization: Bearer <token>
{
"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 ▶
{ "refresh_token": "eyJhbGci..." }{ "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) ▶
| Param | Type | Default | Description |
|---|---|---|---|
status | string | — | pending | issued | revoked |
search | string | — | Search by recipient name or email |
page | integer | 1 | Page number |
limit | integer | 20 | Max 100 |
{
"data": [{ "id": "cert_uuid", "status": "issued", "recipient_email": "...", "issued_at": "..." }],
"total": 150, "page": 1, "limit": 20
}POST /credentials/issue Issue a single certificate immediately ▶
{
"template_id": "template_uuid",
"recipient": {
"name": "John Doe",
"email": "john@example.com",
"metadata": { "course": "Data Science", "grade": "A" }
}
}{ "id": "cert_uuid", "status": "pending", "job_id": "bull_job_id" }| Status | Description |
|---|---|
| 402 | Insufficient credits — top up at POST /payments/orders |
| 403 | KYC not approved — organisation must complete KYC first |
POST /credentials/batches Upload a CSV batch for bulk issuance ▶
Content-Type: multipart/form-data
| Field | Type | Description |
|---|---|---|
file | CSV file | Max 10MB. Columns: name, email, metadata fields matching template |
template_id | string | UUID of the certificate template to use |
{ "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.
{ "batch_id": "batch_uuid", "status": "processing", "jobs_enqueued": 250 }GET /credentials/batches/:batchId Poll batch processing status ▶
{
"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) ▶
{
"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
}{ "valid": false, "revoked": true, "revoked_at": "2026-03-01T09:00:00Z", "reason": "Issued in error" }| Status | Description |
|---|---|
| 404 | Certificate not found |
| 422 | Signature verification failed — cert data may be tampered |
POST /verify/bulk Verify multiple certificates in one request ▶
{ "cert_ids": ["cert_uuid_1", "cert_uuid_2"] }{ "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.
{ "reason": "Issued in error — recipient data incorrect" }{ "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 ▶
{ "cert_id": "cert_uuid", "cert_hash": "0xabc...", "issuer_address": "0x123..." }{ "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 ▶
{ "anchored": true, "issuer_address": "0x123...", "timestamp": 1717416000, "block_number": 12345678 }GET /health Blockchain service health — includes RPC connectivity check ▶
{
"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 ▶
{
"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"
}{ "submission_id": "kyc_uuid", "status": "under_review" }POST /kyc/documents Upload KYC supporting documents (multipart) ▶
Content-Type: multipart/form-data
| Field | Description |
|---|---|
file | PDF or image. Max 10MB per file. |
document_type | coi (Certificate of Incorporation) | pan | gst | address_proof |
{ "document_id": "doc_uuid", "document_type": "coi", "url": "https://storage/..." }GET /kyc/status Get KYC status for the authenticated organisation ▶
{ "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.
{ "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 ▶
{ "pack": "250" }Valid pack values: "50" (₹499), "250" (₹1,999), "750" (₹4,999), "2000" (₹9,999)
{
"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 ▶
{
"razorpay_order_id": "order_rzp_...",
"razorpay_payment_id": "pay_rzp_...",
"razorpay_signature": "hmac_sha256_signature"
}{ "credits_added": 250, "new_balance": 312 }GET /payments/balance Get current credit balance for the organisation ▶
{ "balance": 312, "lifetime_purchased": 500, "lifetime_used": 188 }GET /payments/invoices List payment invoices for the organisation ▶
{ "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) ▶
| Param | Description |
|---|---|
search | Search by org name or domain |
tier | starter | growth | enterprise |
kyc_status | not_submitted | under_review | approved | rejected |
status | active | suspended | blocked |
page, limit | Pagination |
POST /admin/orgs/:id/approve-kyc Approve KYC for an organisation ▶
{ "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 ▶
{ "reason": "Provided CIN does not match company name in MCA records" }POST /admin/orgs/:id/suspend Suspend an organisation (blocks login and API access) ▶
{ "reason": "Policy violation — suspicious issuance pattern" }CERT-In Compliance Endpoints
POST /admin/certin/incident CERT-In CE2 — Report a cybersecurity incident ▶
{
"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 ▶
{ "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▶
{ "event_type": "cert_issued", "payload": { "cert_id": "uuid", "template_id": "uuid" } }{ "event_id": "uuid", "recorded_at": "2026-06-03T12:00:00Z" }GET/analytics/dashboard Get dashboard summary stats for the organisation▶
{
"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▶
{ "data": [{ "id": "moodle", "name": "Moodle LMS", "type": "lms", "active": true }] }POST/marketplace/connectors/:id/activate Activate a connector for the organisation▶
{ "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).
/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▶
{ "subject": "Certificate not received", "description": "Batch 123 shows issued but recipient did not get email", "priority": "medium" }{ "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)▶
| Param | Description |
|---|---|
status | open | in_progress | resolved | closed |
priority | low | 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)▶
{
"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▶
{ "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:
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
- POST to your endpoint with
Content-Type: application/json - 10 second timeout per attempt
- Retried 3 times on non-2xx response or timeout (exponential backoff: 1m, 5m, 30m)
- After 3 failures, event is marked
failedand visible in webhook delivery log
Event Catalog
EVENT cert.issued Fired when a certificate is successfully issued and blockchain-anchored ▶
{
"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 ▶
{ "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 ▶
{ "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 ▶
{ "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 ▶
{ "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.
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)
| Command | Description |
|---|---|
npm run dev | Start all services in watch mode (Turborepo parallel) |
npm run build | Build all packages and services |
npm run test | Run all Jest test suites |
npm run type-check | TypeScript type check across all packages |
npm run lint | ESLint across all packages |
turbo run dev --filter=auth-service | Run only auth-service in dev mode |
Adding a New Service
Follow these steps to add a new NestJS microservice to the FinVault monorepo.
- 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 - Add to turbo.json pipeline: Add
"apps/my-service"to the workspaces array inpackage.jsonroot. - Assign a port — next available after :4014. Update
README.mdservice port map. - Add to docker-compose.yml for local dev (optional, services run natively in dev mode).
- 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' }; } } - Register in api-gateway — add proxy route in
apps/api-gateway/src/index.tspointing to the new service. - 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; } - Add to smoke tests — extend
infra/scripts/smoke-tests.tswith 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.
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.
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.
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
| Layer | Tool | Location | Scope |
|---|---|---|---|
| Unit tests | Jest | apps/*/src/**/*.spec.ts | Individual service methods, DTOs, guards. Uses mocked dependencies. |
| Integration tests | Jest + real Supabase | apps/*/src/**/*.integration.spec.ts | DB queries, RLS policy checks. Requires test Supabase project. |
| Smoke tests | ts-node script | infra/scripts/smoke-tests.ts | 12-test end-to-end flow against running services. |
Current Test Status
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
# 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
// 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
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 serviceDTO Validation Pattern
Always use class-validator decorators. The global ValidationPipe in main.ts handles rejection automatically.
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
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);
}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
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 amountsError Response Format
All services return errors in this format:
{
"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:
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
- Algorithm: HS256 pinned on ALL 5 JWT guards —
{ algorithms: ['HS256'] }always specified - Access token lifetime: 15 minutes
- Refresh token lifetime: 7 days, rotation on every refresh (old token invalidated)
- Refresh tokens stored: SHA-256 hashed in DB — raw token never stored
- Empty secret fallback removed:
INTERNAL_JWT_SECRET=""now throws at startup
Cryptography
| Operation | Algorithm | Implementation |
|---|---|---|
| Certificate signing | RSA-2048 + SHA-256 | @finvault/crypto — signCertificate() |
| Certificate verification | RSA-2048 + SHA-256 | @finvault/crypto — verifyCertificate() |
| Private key encryption at rest | AES-256-GCM | @finvault/crypto — encryptWithAES() |
| Canonical JSON (hash input) | RFC 8785 | @finvault/crypto — canonicalJson() |
| Timing-safe comparison | HMAC-based | @finvault/crypto — timingSafeEqual() |
| TOTP backup codes | bcrypt (rounds=12) | Stored hashed, single-use |
| Refresh tokens (storage) | SHA-256 | Never 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 group | Limit |
|---|---|
Auth endpoints (/auth/*) | 20 req/min per IP |
Verification (/verify/*) | 200 req/min per IP |
Admin (/admin/*) | 30 req/min per IP |
| General API | 100 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.
| Requirement | Description | Implementation | Status |
|---|---|---|---|
| CE1 | 180-day log retention | Migration 0053_certin_log_retention.sql — certin_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 |
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.
| OWASP | Category | Finding | Fix | Status |
|---|---|---|---|---|
| A01 | Broken Access Control | Analytics service routes unauthenticated; CA portal IDOR | Added auth guards to all routes; fixed IDOR with org_id ownership check | ✅ |
| A02 | Cryptographic Failures | Private keys not encrypted at rest in all paths | AES-256-GCM encryption via @finvault/crypto on all key storage | ✅ |
| A03 | Injection | Missing global ValidationPipe; ruleData field injection | Global ValidationPipe added to all services; field whitelist on ruleData | ✅ |
| A04 | Insecure Design | TOTP secret fetched before password validation | Fetch TOTP secret only after password is verified | ✅ |
| A05 | Security Misconfiguration | CORS wildcard; rate limiter JWT claim key bug; duplicate admin limiter | CORS restricted to ALLOWED_ORIGINS; JWT claim key fixed; duplicate removed | ✅ |
| A07 | Authentication Failures | JWT algorithm confusion; account lockout missing; timing-unsafe compare | HS256 pinned on all guards; account lockout via @finvault/abuse; timingSafeEqual | ✅ |
| A08 | Software/Data Integrity | Non-canonical JSON in cert hash; no blockchain cross-check | RFC 8785 canonical JSON; blockchain anchor verified on every verification | ✅ |
| A09 | Logging Failures | No request ID tracing; gaps in audit log coverage | Request ID injected at gateway; propagated via X-Request-ID header; audit_logs table | ✅ |
| A10 | SSRF | PDF background image URL not validated — could fetch internal metadata endpoints | URL allowlist for external domains only; internal IP ranges blocked | ✅ |
| A06 | Vulnerable Components | 17 npm vulnerabilities in audit | Not yet resolved — see Pre-Production Checklist | ⚠ Pending |
Responsible Disclosure
Reporting a Vulnerability
[SECURITY] <brief description>. We will acknowledge within 48 hours and triage within 7 days.Response SLAs
| Severity | Patch Target |
|---|---|
| Critical | 7 days |
| High | 30 days |
| Medium | 90 days |
| Low | Next release |
Out of Scope
- DoS via resource exhaustion
- Social engineering of staff
- Physical security issues
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
supabase db push. All 53 migrations must be applied in order before services start.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.npm audit to see current findings. Upgrade or patch affected packages before production. At minimum, resolve all Critical and High severity vulns.🟡 High — Fix Before First Users
finvault.in sending domain in SendGrid. Without these, emails may be flagged as spam or rejected by recipient mail servers.INTERNAL_JWT_SECRET, STAFF_JWT_SECRET, etc.) that may not be in .env.example. Audit and update.docs/ISSUES.md.supabase db push on the production project.🟢 Done — Security Hardening Complete
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.Go-Live Sign-Off
| # | Item | Owner | Sign-off Date | Notes |
|---|---|---|---|---|
| 1 | Migrations applied to production Supabase | |||
| 2 | Cloud infrastructure provisioned | |||
| 3 | Smart contract audited + deployed to mainnet | |||
| 4 | KMS signer wired | |||
| 5 | npm audit clean (Critical/High) | |||
| 6 | Cloudflare WAF configured | |||
| 7 | Email DNS records (DMARC/SPF/DKIM) set | |||
| 8 | All tests passing (363 / 363) | |||
| 9 | Smoke tests pass against production URL | |||
| 10 | Monitoring and alerting configured |