Xpensio

Technical Documentation

Enterprise Expense Management Platform

Full-Stack Architecture, API Reference & Deployment Guide

Version2.0
DateMarch 2026
StatusProduction
PlatformWeb · Mobile · API
Backendapi.xpensioapp.com
Web Appapp.xpensioapp.com
© 2026 Xpensio — Confidential & Proprietary

Table of Contents

  1. System Architecture
  2. Project Structure
  3. Database Schema
  4. Authentication & Authorization
  5. API Reference
  6. OCR Pipeline
  7. ERP Integration
  8. Security Architecture
  9. Notification System
  10. Environment Variables
  11. Deployment Guide
  12. Error Codes & Troubleshooting
  13. Roadmap

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

LayerTechnologyVersionPurpose
Backend APINestJS10.xREST API, business logic, ERP adapters
Web FrontendNext.js14.x (App Router)Employee/manager/admin dashboard
MobileFlutter3.xiOS & Android employee app
Landing PageNext.js + i18n14.xMarketing site, pricing, blog
DatabasePostgreSQL15Primary data store
ORMPrisma5.8.0Type-safe database access
Package Managerpnpm workspaces8.xMonorepo dependency management
ContainerDocker + Compose24.xProduction deployment on VPS
OCR PrimaryGemini 2.5 FlashAPIReceipt scanning and field extraction
OCR SecondaryGPT-4o VisionAPIFallback when Gemini unavailable
OCR TertiaryTesseract.js5.xOffline fallback OCR
Push NotificationsFirebase Admin SDK12.xMobile push notifications
EmailNodemailer6.xSMTP transactional emails
AuthJWT (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:

StatusDescriptionAllowed TransitionsActor
DRAFTExpense created, not yet submitted→ SUBMITTED, → deletedEmployee
Submitted for approval→ PENDING_MANAGERSystem
PENDING_MANAGERAwaiting manager review→ PENDING_CFO, → PENDING_FINANCE, → REJECTEDManager
PENDING_CFOAwaiting CFO approval (amount > 50,000)→ PENDING_FINANCE, → REJECTEDCFO/Admin
PENDING_FINANCEAwaiting finance processing→ APPROVED, → REJECTEDFinance
APPROVEDFully approved, ready for ERP→ ERP_SENTFinance
REJECTEDRejected at any approval stageTerminal stateManager/Finance/CFO
ERP_SENTPosted to ERP system (SAP/other)Terminal stateSystem

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

FieldTypeNullableDescription
idString (UUID)NoPrimary key, auto-generated CUID
emailStringNoUnique per organization, login identifier
nameStringNoDisplay name
passwordHashStringNobcrypt hash (rounds: 12)
roleEnumNoEMPLOYEE | MANAGER | FINANCE | ADMIN | SUPERUSER
orgIdString (FK)NoTenant identifier — mandatory on all queries
subCompanyIdString (FK)YesSub-company / cost center assignment
gradeStringYesEmployee grade (e.g., "Senior", "L5")
managerIdString (FK → User)YesDirect manager for approval routing
mustChangePasswordBooleanNoForces password change on next login (default: false)
isApprovedBooleanNoAdmin-approved account activation
isEmailConfirmedBooleanNoEmail verification status
isActiveBooleanNoSoft-delete flag (default: true)
fcmTokenStringYesFirebase Cloud Messaging device token
passwordResetTokenStringYesSHA-256 hashed reset token
passwordResetExpiryDateTimeYesToken expiry (1 hour from generation)
createdAtDateTimeNoRecord creation timestamp
updatedAtDateTimeNoLast update timestamp

3.2 Expense Model

FieldTypeNullableDescription
idString (UUID)NoPrimary key
userIdString (FK → User)NoExpense owner
orgIdString (FK → Organization)NoTenant isolation key
amountDecimal(12,2)NoTotal amount including tax
currencyString(3)NoISO 4217 code (TRY, USD, EUR)
taxAmountDecimal(12,2)YesVAT/tax amount extracted from receipt
taxPercentageCodeStringYesERP tax code (V0/V1/V2/V3/V4)
categoryStringNoExpense category (TRAVEL, MEAL, ACCOMMODATION, etc.)
documentTypeStringYesDocument type (INVOICE, RECEIPT, TICKET)
expenseTypeCodeStringYesERP expense type code for GL mapping
vehiclePlateStringYesVehicle plate for fuel/transportation expenses
receiptNumberStringYesReceipt/invoice number — locked after OCR fill
receiptNumberLockedBooleanNoPrevents receipt number modification after OCR
statusEnumNoWorkflow status (see state machine above)
expenseDateDateTimeNoDate on the receipt/invoice
projectCodeStringYesInternal project code for cost allocation
costCenterStringYesERP cost center (KOSTL in SAP)
descriptionStringYesFree-text description
receiptImageUrlStringYesUploaded receipt image URL
fraudScoreIntYesAI fraud detection score 0–100
fraudFlagsJsonYesDetailed fraud analysis flags array
receiptHashStringYesSHA-256 hash for duplicate detection
subCompanyIdString (FK)YesSub-company for ERP posting
approvedAtDateTimeYesFinal approval timestamp
rejectedAtDateTimeYesRejection timestamp
rejectionReasonStringYesRejection comment from approver
erpSentAtDateTimeYesERP posting timestamp
createdAtDateTimeNoRecord creation
updatedAtDateTimeNoLast update

3.3 Organization Model

FieldTypeNullableDescription
idString (UUID)NoPrimary key — used as orgId tenant key
nameStringNoOrganization display name
slugStringNoUnique URL-safe identifier
erpProviderStringYesERP type: SAP | ORACLE | LOGO | NONE
sapClientIdStringYesSAP client/mandant number
sapBaseUrlStringYesSAP system URL for RFC/OData calls
subscriptionPlanStringYesSTARTER | PROFESSIONAL | ENTERPRISE
isActiveBooleanNoOrganization active status
createdAtDateTimeNoOrg creation date

3.4 SubCompany Model

FieldTypeNullableDescription
idString (UUID)NoPrimary key
orgIdString (FK)NoParent organization
nameStringNoSub-company name
erpCompanyCodeStringYesERP Şirket Kodu (SAP BUKRS field)
costCenterStringYesDefault cost center code
isActiveBooleanNoActive status

3.5 RefreshToken Model

FieldTypeNullableDescription
idString (UUID)NoPrimary key
tokenStringNoSHA-256 hashed refresh token
userIdString (FK → User)NoToken owner
expiresAtDateTimeNoExpiry (7 days from issuance)
isRevokedBooleanNoRevoked tokens cannot be refreshed
createdAtDateTimeNoToken creation time

3.6 Notification Model

FieldTypeNullableDescription
idString (UUID)NoPrimary key
userIdString (FK → User)NoNotification recipient
orgIdString (FK)NoTenant isolation
typeStringNoEXPENSE_SUBMITTED | APPROVED | REJECTED | ERP_SENT
titleStringNoNotification title
bodyStringNoNotification body text
isReadBooleanNoRead status (default: false)
expenseIdString (FK)YesRelated expense for deep linking
createdAtDateTimeNoNotification creation time

14. Release Notes (v1.4 — April 2026)

ModuleWhat's new
SAP HRUser costCenterCode auto-synced daily via SAP userlist; fallback chain OPHCODE / KOSTL / COSTCENTER / RELID
SAP OAuth2Dynamic token flow: POST /sap-auth/token, SapJwtGuard, ABAP ZCL_XPENSIO_AUTH + ZXPENSIO_TOKEN; client_id/secret managed in ZEXP_CFG_01
SAP TLSSetup wizard custom CA certificate (PEM) upload; Axios https.Agent({ ca }) for one-way TLS
Setup WizardHR / Finance endpoint paths overridable per tenant in separate tabs
Expense SnapshotsnapshotCostCenter*, snapshotDepartment*, snapshotPosition*, snapshotUpperManager*, snapshotCapturedAt captured at submit — reports stay accurate after employee data changes
TESLIM_ALINDINew intermediate ExpenseStatus. Report PDF carries QR xpensio://receive/<id>?t=<token>, scanned by finance via mobile. Endpoint POST /reports/:id/receive
Mobilev1.4.0+40: mobile_scanner-based receive screen, restricted to FINANCE/ADMIN
UserChangeHistoryField-level diff tracking (department, manager, cost center, position, role, grade…). Source: SAP_SYNC | ADMIN. GET /users/:id/history + admin UI history tab
Menu VisibilityOrg Chart, Positions, Companies, Cost Centers, Setup, Fraud, Audit Logs → super admin only. Backend SuperAdminGuard + Sidebar superAdminOnly filter
Grade PoliciesPer-company grade spending limits (grade_policies.company_id)
Report NumberingOrg-wide single increasing report number sequence