Xpensio
Technical Documentation
Enterprise Expense Management Platform
Full-Stack Architecture, API Reference & Deployment Guide
© 2026 Xpensio — Confidential & Proprietary
1.System Architecture
Xpensio is a cloud-native, multi-tenant enterprise expense management platform built as a monorepo. It consists of four distinct application layers that communicate over HTTPS REST APIs, with shared infrastructure for authentication, notifications, and ERP integration.
1.1 High-Level Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ XPENSIO PLATFORM │
├─────────────────┬────────────────────┬──────────────────────────────────┤
│ Web App │ Mobile App │ Landing Page │
│ Next.js 14 │ Flutter 3 │ Next.js + i18n │
│ Vercel CDN │ iOS + Android │ Vercel CDN │
│ app.xpensio.. │ App Store/Play │ xpensioapp.com │
├─────────────────┴────────────────────┴──────────────────────────────────┤
│ HTTPS REST API (TLS 1.3) │
├─────────────────────────────────────────────────────────────────────────┤
│ NestJS 10 Backend │
│ api.xpensioapp.com — Port 3001 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Auth │ │ Users │ │ Expenses │ │ Receipts │ │ ERP │ │
│ │ Module │ │ Module │ │ Module │ │ OCR │ │ Adapter │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ Infrastructure Services │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │PostgreSQL│ │ Redis │ │ Firebase │ │ SMTP │ │ Gemini/ │ │
│ │ (Prisma)│ │ (cache) │ │ Push │ │ Email │ │ OpenAI │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
1.2 Technology Stack
| Layer | Technology | Version | Purpose |
| Backend API | NestJS | 10.x | REST API, business logic, ERP adapters |
| Web Frontend | Next.js | 14.x (App Router) | Employee/manager/admin dashboard |
| Mobile | Flutter | 3.x | iOS & Android employee app |
| Landing Page | Next.js + i18n | 14.x | Marketing site, pricing, blog |
| Database | PostgreSQL | 15 | Primary data store |
| ORM | Prisma | 5.8.0 | Type-safe database access |
| Package Manager | pnpm workspaces | 8.x | Monorepo dependency management |
| Container | Docker + Compose | 24.x | Production deployment on VPS |
| OCR Primary | Gemini 2.5 Flash | API | Receipt scanning and field extraction |
| OCR Secondary | GPT-4o Vision | API | Fallback when Gemini unavailable |
| OCR Tertiary | Tesseract.js | 5.x | Offline fallback OCR |
| Push Notifications | Firebase Admin SDK | 12.x | Mobile push notifications |
| Email | Nodemailer | 6.x | SMTP transactional emails |
| Auth | JWT (RS256) | — | Stateless authentication |
1.3 Infrastructure
Hetzner VPS (Backend)
- OS: Ubuntu 22.04 LTS
- IP: 89.167.115.209
- Runtime: Docker Compose
- Services: backend + PostgreSQL + Nginx
- Deploy: rsync + docker build
- Endpoint: api.xpensioapp.com
Vercel (Frontend)
- Web App: app.xpensioapp.com
- Landing: xpensioapp.com
- Deploy: git push → auto CI/CD
- Edge CDN: Global distribution
- SSL: Automatic Let's Encrypt
- Framework: Next.js serverless
1.4 Multi-Tenancy Model
Xpensio uses a shared database, shared schema multi-tenancy approach. Every database record contains an orgId field that is automatically injected and validated on every query. A middleware layer extracts the orgId from the JWT token and verifies it against the requested resource, making cross-organization data leakage structurally impossible at the service layer.
Tenant Isolation: The orgId from the JWT payload is treated as the security boundary. All Prisma queries include where: { orgId: req.user.orgId } as a mandatory filter. This is enforced by NestJS guards and verified in E2E tests.
1.5 Expense Workflow State Machine
Every expense follows a strict state machine with role-based transitions:
| Status | Description | Allowed Transitions | Actor |
| DRAFT | Expense created, not yet submitted | → SUBMITTED, → deleted | Employee |
| SUBMITTED | Submitted for approval | → PENDING_MANAGER | System |
| PENDING_MANAGER | Awaiting manager review | → PENDING_CFO, → PENDING_FINANCE, → REJECTED | Manager |
| PENDING_CFO | Awaiting CFO approval (amount > 50,000) | → PENDING_FINANCE, → REJECTED | CFO/Admin |
| PENDING_FINANCE | Awaiting finance processing | → APPROVED, → REJECTED | Finance |
| APPROVED | Fully approved, ready for ERP | → ERP_SENT | Finance |
| REJECTED | Rejected at any approval stage | Terminal state | Manager/Finance/CFO |
| ERP_SENT | Posted to ERP system (SAP/other) | Terminal state | System |
2.Project Structure
The project is a pnpm monorepo with four workspace packages. Each package is independently deployable and has its own dependencies managed via pnpm workspaces.
2.1 Repository Root
Claude_Proj1/ # Repository root
├── apps/
│ ├── backend/ # NestJS API server
│ ├── web/ # Next.js web dashboard
│ ├── mobile/
│ │ └── expense_mobile/ # Flutter mobile app
│ └── website/ # Next.js marketing site
│ └── src/
├── website/ # Landing page (Next.js)
├── docs/ # Technical documentation
├── docker-compose.yml # Local development stack
├── docker-compose.prod.yml # Production override (VPS)
├── package.json # Root workspace config
├── pnpm-workspace.yaml # pnpm workspace definition
└── .env # Root environment variables
2.2 Backend Structure (apps/backend/)
apps/backend/
├── src/
│ ├── main.ts # NestJS bootstrap, CORS, Swagger
│ ├── app.module.ts # Root module, global guards
│ ├── auth/
│ │ ├── auth.module.ts
│ │ ├── auth.service.ts # Login, register, token management
│ │ ├── auth.controller.ts # /auth/* endpoints
│ │ ├── jwt.strategy.ts # JWT validation strategy
│ │ ├── jwt-refresh.strategy.ts # Refresh token strategy
│ │ └── guards/
│ │ ├── jwt-auth.guard.ts
│ │ └── roles.guard.ts
│ ├── users/
│ │ ├── users.module.ts
│ │ ├── users.service.ts # User CRUD, import, password reset
│ │ └── users.controller.ts # /users/* endpoints
│ ├── expenses/
│ │ ├── expenses.module.ts
│ │ ├── expenses.service.ts # Core workflow, fraud detection
│ │ ├── expenses.controller.ts # /expenses/* endpoints
│ │ └── dto/
│ ├── receipts/
│ │ ├── receipts.module.ts
│ │ ├── receipts.service.ts # OCR pipeline orchestration
│ │ └── receipts.controller.ts # /receipts/scan endpoint
│ ├── notifications/
│ │ ├── notifications.module.ts
│ │ ├── notifications.service.ts # Email + push dispatch
│ │ └── notifications.controller.ts
│ ├── identity/
│ │ ├── identity.module.ts
│ │ ├── identity.service.ts # IDENTITY_PROVIDER routing
│ │ └── adapters/
│ │ ├── sap.adapter.ts # SAP ECC/S4HANA RFC adapter
│ │ ├── oracle.adapter.ts # Oracle ERP adapter
│ │ ├── logo.adapter.ts # Logo Tiger adapter
│ │ └── none.adapter.ts # No-op adapter (dev/default)
│ └── common/
│ ├── decorators/
│ ├── filters/ # Global exception filter
│ ├── interceptors/ # Logging, transform
│ └── pipes/ # Validation pipe
├── prisma/
│ ├── schema.prisma # Database schema (source of truth)
│ ├── migrations/ # Prisma migration history
│ ├── seed.ts # Development seed
│ └── seed-demo.js # Demo org seeder
├── Dockerfile # Multi-stage production build
├── .env # Backend environment variables
└── tsconfig.json
2.3 Web Frontend Structure (apps/web/)
apps/web/
├── src/
│ ├── app/ # Next.js 14 App Router
│ │ ├── layout.tsx # Root layout, providers
│ │ ├── page.tsx # Landing redirect
│ │ ├── login/page.tsx # Login page
│ │ ├── forgot-password/ # Password reset flow
│ │ └── dashboard/
│ │ ├── layout.tsx # Dashboard shell + sidebar
│ │ ├── expenses/page.tsx # Expense list + actions
│ │ ├── expenses/new/page.tsx # New expense form
│ │ ├── approvals/page.tsx # Manager approval queue
│ │ ├── finance/page.tsx # Finance processing
│ │ ├── reports/page.tsx # Reports + Excel export
│ │ ├── users/page.tsx # Admin user management
│ │ ├── settings/page.tsx # User settings + password
│ │ └── admin/page.tsx # Org configuration
│ ├── components/ # Shared UI components
│ │ ├── ExpenseCard.tsx
│ │ ├── ApprovalQueue.tsx
│ │ ├── NotificationBell.tsx
│ │ └── UserImportModal.tsx
│ ├── lib/
│ │ ├── api.ts # Axios API client + interceptors
│ │ └── store.ts # Zustand global state
│ └── types/ # TypeScript type definitions
├── e2e/
│ ├── helpers.ts # Shared Playwright helpers
│ ├── production/ # Production E2E test suites
│ └── playwright.config.ts
└── next.config.js
2.4 Mobile Structure (apps/mobile/expense_mobile/)
expense_mobile/
├── lib/
│ ├── main.dart # App entry point, Firebase init
│ ├── screens/
│ │ ├── login_screen.dart
│ │ ├── expenses_screen.dart # Expense list
│ │ ├── new_expense_screen.dart # Create expense + OCR
│ │ ├── approvals_screen.dart # Manager approvals
│ │ ├── companies_screen.dart # Sub-company selector
│ │ └── profile_screen.dart # User settings
│ ├── services/
│ │ ├── api_service.dart # HTTP client
│ │ ├── auth_service.dart # Token management
│ │ └── notification_service.dart # FCM push handling
│ └── widgets/ # Reusable Flutter widgets
├── ios/
│ ├── Runner/
│ │ └── Info.plist # iOS permissions (camera, photo)
│ └── Podfile
├── android/
│ └── app/
│ ├── build.gradle
│ └── google-services.json # Firebase config (Android)
└── pubspec.yaml # Flutter dependencies
3.Database Schema
Xpensio uses PostgreSQL 15 with Prisma ORM (v5.8.0). The schema enforces multi-tenancy at the data layer via orgId foreign keys. All migrations are tracked in apps/backend/prisma/migrations/.
3.1 User Model
| Field | Type | Nullable | Description |
id | String (UUID) | No | Primary key, auto-generated CUID |
email | String | No | Unique per organization, login identifier |
name | String | No | Display name |
passwordHash | String | No | bcrypt hash (rounds: 12) |
role | Enum | No | EMPLOYEE | MANAGER | FINANCE | ADMIN | SUPERUSER |
orgId | String (FK) | No | Tenant identifier — mandatory on all queries |
subCompanyId | String (FK) | Yes | Sub-company / cost center assignment |
grade | String | Yes | Employee grade (e.g., "Senior", "L5") |
managerId | String (FK → User) | Yes | Direct manager for approval routing |
mustChangePassword | Boolean | No | Forces password change on next login (default: false) |
isApproved | Boolean | No | Admin-approved account activation |
isEmailConfirmed | Boolean | No | Email verification status |
isActive | Boolean | No | Soft-delete flag (default: true) |
fcmToken | String | Yes | Firebase Cloud Messaging device token |
passwordResetToken | String | Yes | SHA-256 hashed reset token |
passwordResetExpiry | DateTime | Yes | Token expiry (1 hour from generation) |
createdAt | DateTime | No | Record creation timestamp |
updatedAt | DateTime | No | Last update timestamp |
3.2 Expense Model
| Field | Type | Nullable | Description |
id | String (UUID) | No | Primary key |
userId | String (FK → User) | No | Expense owner |
orgId | String (FK → Organization) | No | Tenant isolation key |
amount | Decimal(12,2) | No | Total amount including tax |
currency | String(3) | No | ISO 4217 code (TRY, USD, EUR) |
taxAmount | Decimal(12,2) | Yes | VAT/tax amount extracted from receipt |
taxPercentageCode | String | Yes | ERP tax code (V0/V1/V2/V3/V4) |
category | String | No | Expense category (TRAVEL, MEAL, ACCOMMODATION, etc.) |
documentType | String | Yes | Document type (INVOICE, RECEIPT, TICKET) |
expenseTypeCode | String | Yes | ERP expense type code for GL mapping |
vehiclePlate | String | Yes | Vehicle plate for fuel/transportation expenses |
receiptNumber | String | Yes | Receipt/invoice number — locked after OCR fill |
receiptNumberLocked | Boolean | No | Prevents receipt number modification after OCR |
status | Enum | No | Workflow status (see state machine above) |
expenseDate | DateTime | No | Date on the receipt/invoice |
projectCode | String | Yes | Internal project code for cost allocation |
costCenter | String | Yes | ERP cost center (KOSTL in SAP) |
description | String | Yes | Free-text description |
receiptImageUrl | String | Yes | Uploaded receipt image URL |
fraudScore | Int | Yes | AI fraud detection score 0–100 |
fraudFlags | Json | Yes | Detailed fraud analysis flags array |
receiptHash | String | Yes | SHA-256 hash for duplicate detection |
subCompanyId | String (FK) | Yes | Sub-company for ERP posting |
approvedAt | DateTime | Yes | Final approval timestamp |
rejectedAt | DateTime | Yes | Rejection timestamp |
rejectionReason | String | Yes | Rejection comment from approver |
erpSentAt | DateTime | Yes | ERP posting timestamp |
createdAt | DateTime | No | Record creation |
updatedAt | DateTime | No | Last update |
3.3 Organization Model
| Field | Type | Nullable | Description |
id | String (UUID) | No | Primary key — used as orgId tenant key |
name | String | No | Organization display name |
slug | String | No | Unique URL-safe identifier |
erpProvider | String | Yes | ERP type: SAP | ORACLE | LOGO | NONE |
sapClientId | String | Yes | SAP client/mandant number |
sapBaseUrl | String | Yes | SAP system URL for RFC/OData calls |
subscriptionPlan | String | Yes | STARTER | PROFESSIONAL | ENTERPRISE |
isActive | Boolean | No | Organization active status |
createdAt | DateTime | No | Org creation date |
3.4 SubCompany Model
| Field | Type | Nullable | Description |
id | String (UUID) | No | Primary key |
orgId | String (FK) | No | Parent organization |
name | String | No | Sub-company name |
erpCompanyCode | String | Yes | ERP Şirket Kodu (SAP BUKRS field) |
costCenter | String | Yes | Default cost center code |
isActive | Boolean | No | Active status |
3.5 RefreshToken Model
| Field | Type | Nullable | Description |
id | String (UUID) | No | Primary key |
token | String | No | SHA-256 hashed refresh token |
userId | String (FK → User) | No | Token owner |
expiresAt | DateTime | No | Expiry (7 days from issuance) |
isRevoked | Boolean | No | Revoked tokens cannot be refreshed |
createdAt | DateTime | No | Token creation time |
3.6 Notification Model
| Field | Type | Nullable | Description |
id | String (UUID) | No | Primary key |
userId | String (FK → User) | No | Notification recipient |
orgId | String (FK) | No | Tenant isolation |
type | String | No | EXPENSE_SUBMITTED | APPROVED | REJECTED | ERP_SENT |
title | String | No | Notification title |
body | String | No | Notification body text |
isRead | Boolean | No | Read status (default: false) |
expenseId | String (FK) | Yes | Related expense for deep linking |
createdAt | DateTime | No | Notification creation time |
14. Release Notes (v1.4 — April 2026)
| Module | What's new |
| SAP HR | User costCenterCode auto-synced daily via SAP userlist; fallback chain OPHCODE / KOSTL / COSTCENTER / RELID |
| SAP OAuth2 | Dynamic token flow: POST /sap-auth/token, SapJwtGuard, ABAP ZCL_XPENSIO_AUTH + ZXPENSIO_TOKEN; client_id/secret managed in ZEXP_CFG_01 |
| SAP TLS | Setup wizard custom CA certificate (PEM) upload; Axios https.Agent({ ca }) for one-way TLS |
| Setup Wizard | HR / Finance endpoint paths overridable per tenant in separate tabs |
| Expense Snapshot | snapshotCostCenter*, snapshotDepartment*, snapshotPosition*, snapshotUpperManager*, snapshotCapturedAt captured at submit — reports stay accurate after employee data changes |
| TESLIM_ALINDI | New intermediate ExpenseStatus. Report PDF carries QR xpensio://receive/<id>?t=<token>, scanned by finance via mobile. Endpoint POST /reports/:id/receive |
| Mobile | v1.4.0+40: mobile_scanner-based receive screen, restricted to FINANCE/ADMIN |
| UserChangeHistory | Field-level diff tracking (department, manager, cost center, position, role, grade…). Source: SAP_SYNC | ADMIN. GET /users/:id/history + admin UI history tab |
| Menu Visibility | Org Chart, Positions, Companies, Cost Centers, Setup, Fraud, Audit Logs → super admin only. Backend SuperAdminGuard + Sidebar superAdminOnly filter |
| Grade Policies | Per-company grade spending limits (grade_policies.company_id) |
| Report Numbering | Org-wide single increasing report number sequence |