Skip to content

Technical Architecture


┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ Next.js 14 │────▶│ Express API │────▶│ PostgreSQL │
│ (Frontend) │◀────│ (Backend) │◀────│ │
│ App Router │ │ TypeScript/ESM │ └──────────────┘
│ Tailwind CSS │ └────────┬──────────┘
│ shadcn/ui │ │
└──────────────────┘ ┌────────┴──────────┐
│ Redis + BullMQ │
│ (cache + jobs) │
└───────────────────┘
  • App Router with server components for dashboard performance
  • Tailwind CSS + shadcn/ui for consistent, professional UI
  • React Query (TanStack Query) for API state management
  • Recharts or Chart.js for dashboard visualizations
  • React Table (TanStack Table) for sortable/filterable invoice lists
  • React Hook Form + Zod for form validation
  • Express.js with TypeScript (ESM modules)
  • Prisma ORM with PostgreSQL
  • Zod for request validation
  • JWT authentication with refresh tokens
  • Role-based middleware for access control
  • Primary data store for all entities
  • JSON columns for flexible fields (settings, attachments, metadata)
  • Indexed for common query patterns (vendor lookups, date ranges, status filters)
  • Validation engine runs as background job for large invoices
  • Report generation (PDF export) runs async
  • Email notifications queued and sent in background
  • Usage summary recalculation after invoice approval
  • S3-compatible storage (AWS S3 or MinIO)
  • For invoice attachments, supporting documents, generated reports
  • @react-pdf/renderer or Puppeteer for report PDF export

Approach: Shared Database, Row-Level Isolation

Section titled “Approach: Shared Database, Row-Level Isolation”

All tenants share a single database. Every table includes an organizationId column. All queries are scoped.

Why this approach:

  • Simpler to maintain than database-per-tenant
  • Lower infrastructure cost for MVP
  • Scales well up to ~1,000 tenants
  • Can migrate to database-per-tenant later for enterprise clients

Implementation:

  • Prisma middleware that automatically adds organizationId filter to every query
  • API middleware that extracts organizationId from authenticated user’s JWT
  • Database-level row security policies as an additional safety layer (optional)
// Prisma middleware example
prisma.$use(async (params, next) => {
// Automatically scope all queries by organization
if (params.args.where) {
params.args.where.organizationId = currentOrganizationId;
}
return next(params);
});
  1. Organization created with admin user
  2. Admin configures:
    • Organization settings (currency, thresholds, notifications)
    • Users and roles
    • Vendors
    • Contracts and rate cards
  3. System ready to process invoices

/api/v1/{resource}

Authentication

POST /api/v1/auth/register (create organization + admin)
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/forgot-password
POST /api/v1/auth/reset-password

Organization

GET /api/v1/organization
PUT /api/v1/organization
GET /api/v1/organization/settings
PUT /api/v1/organization/settings

Users

GET /api/v1/users
POST /api/v1/users
GET /api/v1/users/:id
PUT /api/v1/users/:id
DELETE /api/v1/users/:id

Vendors

GET /api/v1/vendors
POST /api/v1/vendors
GET /api/v1/vendors/:id
PUT /api/v1/vendors/:id
DELETE /api/v1/vendors/:id
GET /api/v1/vendors/:id/invoices
GET /api/v1/vendors/:id/scorecard

Contracts

GET /api/v1/contracts
POST /api/v1/contracts
GET /api/v1/contracts/:id
PUT /api/v1/contracts/:id
DELETE /api/v1/contracts/:id
GET /api/v1/contracts/:id/rate-cards
POST /api/v1/contracts/:id/rate-cards
PUT /api/v1/contracts/:id/rate-cards/:rateId
DELETE /api/v1/contracts/:id/rate-cards/:rateId
GET /api/v1/contracts/:id/expense-rules
POST /api/v1/contracts/:id/expense-rules
PUT /api/v1/contracts/:id/expense-rules/:ruleId
DELETE /api/v1/contracts/:id/expense-rules/:ruleId
GET /api/v1/contracts/:id/utilization

Invoices

GET /api/v1/invoices
POST /api/v1/invoices
GET /api/v1/invoices/:id
PUT /api/v1/invoices/:id
DELETE /api/v1/invoices/:id
POST /api/v1/invoices/:id/line-items (batch add)
PUT /api/v1/invoices/:id/line-items/:itemId
DELETE /api/v1/invoices/:id/line-items/:itemId
POST /api/v1/invoices/:id/validate (trigger validation)
GET /api/v1/invoices/:id/findings
POST /api/v1/invoices/:id/approve
POST /api/v1/invoices/:id/reject
POST /api/v1/invoices/:id/query
GET /api/v1/invoices/:id/queries
POST /api/v1/invoices/:id/queries/:queryId/respond

Findings

GET /api/v1/findings (with filters)
PUT /api/v1/findings/:id/acknowledge
PUT /api/v1/findings/:id/resolve
PUT /api/v1/findings/:id/waive (requires reason)

Dashboard & Reports

GET /api/v1/dashboard/summary
GET /api/v1/dashboard/findings-by-type
GET /api/v1/dashboard/top-flagged-vendors
GET /api/v1/dashboard/savings
GET /api/v1/dashboard/trends
GET /api/v1/reports/monthly/:year/:month
GET /api/v1/reports/vendor-scorecard/:vendorId
POST /api/v1/reports/export (generates PDF)

Audit Trail

GET /api/v1/audit-log (with filters)

  • JWT with short-lived access tokens (15 min) and refresh tokens (7 days)
  • Refresh token rotation (new refresh token on each use)
  • Password hashing with bcrypt (cost factor 12)
  • Role-based access control enforced at API middleware level
  • Approval thresholds enforced server-side (never trust client)
  • Dual sign-off logic enforced server-side
  • All API communication over HTTPS
  • Sensitive fields encrypted at rest (optional, for enterprise tier)
  • Audit log is append-only (no updates, no deletes)
  • Database backups (daily, retained 30 days minimum)
  • Organization ID extracted from JWT, never from client request
  • Prisma middleware enforces tenant isolation on every query
  • API tests verify tenant isolation (user from org A cannot access org B data)

PhaseScopeDetails
Phase 1FoundationProject setup, Prisma schema, database, auth, multi-tenant middleware
Phase 2Vendor & ContractsCRUD for vendors, contracts, rate cards, expense rules (API + UI)
Phase 3Invoice CaptureInvoice and line item CRUD, manual entry forms, CSV upload
Phase 4Validation EngineAll 14 checks, finding generation, risk scoring
Phase 5Review WorkflowFinding display, approve/reject/query flow, audit trail
Phase 6DashboardExecutive dashboard, contract utilization, charts
Phase 7ReportingMonthly report generation, PDF export, savings tracking
Phase 8Polish & TestingE2E tests, tenant isolation tests, UI polish, performance

#DecisionOptionsStatus
1Multi-tenant approachShared DB with row isolation (recommended) vs DB per tenantDecided: Shared DB
2Invoice input method (MVP)Manual form only vs Manual + CSV upload vs Manual + CSV + PDF uploadPending
3Notification deliveryIn-app only vs In-app + emailPending
4Vendor portalNo portal (email only) vs Basic portal for query responsesPending
5Currency supportSingle currency (ZAR) vs Multi-currencyPending
6HostingSelf-hosted VPS vs AWS/GCP vs Vercel + managed DBPending
7Pricing modelPer-user vs Per-invoice volume vs Flat tiersPending
8Domain / brandingProduct name, domainPending

┌─────────────────────────────────────────┐
│ Vercel (Frontend) │
│ - Next.js deployment │
│ - Edge functions for auth │
│ - CDN for static assets │
├─────────────────────────────────────────┤
│ Railway / Render (Backend) │
│ - Express API │
│ - Auto-scaling │
│ - Managed Redis │
├─────────────────────────────────────────┤
│ Supabase / Neon (Database) │
│ - Managed PostgreSQL │
│ - Automated backups │
│ - Connection pooling │
├─────────────────────────────────────────┤
│ AWS S3 (File Storage) │
│ - Invoice attachments │
│ - Generated reports │
├─────────────────────────────────────────┤
│ Resend / SendGrid (Email) │
│ - Transactional emails │
│ - Notification delivery │
└─────────────────────────────────────────┘

Alternative: Self-Hosted (Lower Cost, More Control)

Section titled “Alternative: Self-Hosted (Lower Cost, More Control)”
  • VPS (Hetzner / DigitalOcean) with Docker Compose
  • Nginx reverse proxy
  • PostgreSQL + Redis on same server (small scale)
  • MinIO for S3-compatible storage