Skip to content

Backend Architecture Overview

The Upvendo backend is a Laravel 11 application backed by MongoDB, Redis, and a queue system. It serves as the central API for all Upvendo products: the Backoffice dashboard, the Kiosk ordering application, the Online Ordering (Zestidoo) platform, the Kitchen Display System, and third-party integrations.


Technology Stack

ComponentTechnology
FrameworkLaravel 11 (PHP 8.2+)
DatabaseMongoDB (via mongodb/laravel-mongodb)
Cache / QueueRedis (Predis client)
AuthenticationCustom JWT + OTP + Passkey (WebAuthn)
Image StorageCloudflare Images
SMSSMS service (for OTP)
Push NotificationsFirebase Cloud Messaging (FCM)
Payment ProvidersStripe, Viva Wallet
Third-Party POSSquare, Kassanet, Hendrickx, Vanhoutte, Shopify
DeliveryUberEats, Deliveroo
MonitoringSentry, custom health checks

Directory Structure

The application follows Laravel conventions enhanced with domain-driven organization.

upvendo-backend/
  app/
    Console/Commands/         # Artisan commands (including DataRepair/)
    Constants/                # Application constants and enums
    Contracts/                # Interfaces and contracts
    Enums/                    # PHP enums (DiningOptions, OrderStatuses, etc.)
    HealthChecks/             # Health check implementations
    Helpers/                  # Utility helper functions
    Http/
      Controllers/Api/        # API controllers organized by domain
        BackOffice/            # Backoffice controllers (+ Settings/)
      Middleware/              # HTTP middleware (auth, tenant, permissions)
      Requests/                # Form request validation classes by domain
      Resources/               # API resource transformers by domain
    Kassanet/                 # Kassanet integration (Factories/, Models/)
    Models/                   # Eloquent models (legacy, being replaced)
    Providers/                # Service providers
    RawFactories/             # Factory classes for creating domain objects
    RawModels/                # Domain model classes (plain PHP objects)
      SubModels/              # Nested value objects
      Traits/                 # Shared model traits
    Repositories/             # Repository pattern implementations
      Traits/                 # Reusable repository traits
    Rules/                    # Custom validation rules
    Services/                 # Business logic layer (see below)
    Traits/                   # Application-wide traits
    Transformers/             # Data transformation utilities
  config/                     # Laravel configuration
  database/                   # Migrations and seeders
  resources/
    lang/                     # Internationalization files
    views/                    # Blade templates (emails, PDFs, notifications)
  routes/
    api/                      # Modular API route files
      backoffice/             # Backoffice-specific routes
    api.php                   # Main API route aggregator
    guest.php                 # Unauthenticated routes
  tests/
    Feature/                  # Feature tests
    Unit/Services/            # Unit tests for services

Service-Orchestrator Pattern

The backend uses a Service-Orchestrator pattern to organize business logic. This is the most important architectural pattern in the codebase.

Services

Services are single-responsibility classes that handle CRUD operations and domain-specific logic for one entity type. They live in app/Services/ and are organized by domain.

Naming convention: {Entity}Service.php

Example: app/Services/BackOffice/ItemService.php handles item creation, updating, deletion, and querying.

Services should:

  • Handle operations for a single domain entity
  • Contain business rules specific to that entity
  • Call repositories for database access
  • Not orchestrate complex multi-entity workflows

Orchestrators

Orchestrators coordinate multiple services to handle complex workflows that span multiple entities or require multi-step operations. They live in app/Services/Orchestrators/.

Naming convention: {Entity}Orchestrator.php or {Domain}Orchestrator.php

Example workflow -- Creating a menu item through the BackOffice:

Controller (receives HTTP request)
  -> ItemOrchestrator (coordinates the workflow)
      -> ItemService (creates/updates the item)
      -> ContentService (handles image/content upload)
      -> InventoryService (initializes stock tracking)
      -> DisplayGroupService (assigns to display groups)
      -> ModifierGroupService (links modifier groups)

Orchestrators Directory

app/Services/Orchestrators/
  AppUpdateOrchestrator.php
  CustomerOrchestrator.php
  DeliverooWebhookOrchestrator.php
  DynamicConstantOrchestrator.php
  GenerateDescriptionOrchestrator.php
  KioskOrchestrator.php
  KitchenDisplayOrchestrator.php
  OnlineOrderingOrchestrator.php
  PhotoStudioOrchestrator.php
  StripeWebhookOrchestrator.php
  TaxRateOrchestrator.php
  UberEatsWebhookOrchestrator.php
  VivaWebhookOrchestrator.php
  BackOffice/
    CategoryOrchestrator.php
    ContentOrchestrator.php
    CustomerOrchestrator.php
    DeliverooIntegrationOrchestrator.php
    DeviceOrchestrator.php
    DeviceProfileOrchestrator.php
    DisplayGroupOrchestrator.php
    InHouseSettingsOrchestrator.php
    InventoryOrchestrator.php
    ItemOrchestrator.php
    LoyaltyOrchestrator.php
    MenuOrchestrator.php
    ModifierGroupOrchestrator.php
    ModifierOrchestrator.php
    OfferOrchestrator.php
    OnlineOrderingOrchestrator.php
    QrOrderingOrchestrator.php
    ShopifyIntegrationOrchestrator.php
    TableSectionOrchestrator.php
    TaxRateOrchestrator.php
    UberEatsIntegrationOrchestrator.php
    VariantGroupOrchestrator.php
    Settings/
      ActivityLogOrchestrator.php
      BillingProfileOrchestrator.php
      BrandingProfileOrchestrator.php
      GuidedSetupOrchestrator.php
      LanguageOrchestrator.php
      PaymentProfileOrchestrator.php
      ReceiptSettingOrchestrator.php
      TranslationOrchestrator.php

Request-Resource Pattern

The HTTP layer uses two complementary classes:

  • Requests (app/Http/Requests/): Validate incoming data. Each domain has its own folder of request classes.
  • Resources (app/Http/Resources/): Transform models into JSON API responses. Mirror the request folder structure.

Controllers should be thin -- they validate via Request classes, delegate to Orchestrators/Services, and transform via Resource classes.


Multi-Tenant Architecture

Upvendo is a multi-tenant SaaS platform. Each vendor (merchant) has its own MongoDB database for tenant-specific data, while shared data lives in a central upvendo database.

Database Connections

Defined in config/database.php:

ConnectionPurpose
mongodbCentral/shared database (vendors, users, locations, roles)
tenantPer-vendor database, set dynamically at runtime

The tenant connection has database: null in the config -- it is populated at runtime by the SetTenantDatabase middleware.

How Tenant Switching Works

  1. JWT token contains tenant_database in its payload.
  2. SetTenantDatabase middleware extracts the tenant database name and calls setTenantDatabase() to configure the tenant connection.
  3. All subsequent queries on the tenant connection target that vendor's database.

The middleware handles three routing strategies:

TypeStrategy
backofficeExtract tenant_database from JWT payload
kiosk / kdsExtract tenant_database from JWT payload
online-orderingLook up tenant from request slug parameter
customerLook up tenant from request locationId parameter

Data Separation

Central database (mongodb connection):

  • users -- Backoffice user accounts
  • vendors -- Merchant/vendor records (contains database_name field)
  • locations -- Physical location records
  • roles -- Permission roles
  • payment_profiles -- Payment configuration
  • customers -- Global customer records

Tenant database (tenant connection):

  • menus -- Polymorphic collection (items, menus, modifiers, modifier groups share this)
  • settings -- Polymorphic collection (categories, languages, display groups, etc.)
  • transactions -- Order/transaction records
  • transaction_items -- Line items within transactions
  • inventories -- Stock/inventory tracking
  • branding_profiles -- Visual branding per tenant
  • billing_profiles -- Billing configuration
  • customer_vendors -- Vendor-specific customer data
  • loyalty -- Loyalty program configuration
  • offers -- Promotional offers
  • devices -- Registered kiosk devices
  • device_profiles -- Device configuration profiles
  • subscriptions -- Active subscriptions

The Vendor Model and Tenancy

The Vendor model (in app/RawModels/Vendor.php) uses the TenancyTrait and has a database_name field. When accessing tenant-specific data through a Vendor, it automatically switches the tenant database:

php
// Vendor sets the tenant DB before querying related data
private function setTenantDb(): void
{
    if (! config('database.connections.tenant.database')) {
        $this->setTenantDatabase($this->database_name);
    }
}

Authentication Layers

The backend supports multiple authentication mechanisms, each targeting a different consumer type.

JWT Authentication

The primary auth mechanism. The JwtService issues and validates JWT tokens. Tokens contain:

  • User identity (user ID, vendor ID)
  • Tenant database name
  • Role/permission claims

Middleware: JwtAuthenticate

OTP (One-Time Password)

Used as a second factor for backoffice login. The User model tracks:

  • otp -- Current OTP code
  • last_otp_request -- Timestamp of last OTP request
  • resend_counter -- Rate limiting counter

Passkey / WebAuthn

Modern passwordless authentication. The User model stores:

  • webauthn_challenge -- Current challenge for registration/authentication
  • passkeys -- Array of registered passkey credentials

Device Authentication

Used by Kiosk devices. The AuthDeviceService handles device-specific auth flows. Devices authenticate via the CapacitorApiKey middleware header.

Trusted Devices

The User model maintains a trusted_devices array. Trust is based on IP + User-Agent fingerprinting with a 30-day expiration.

Challenge Tokens

For sensitive operations, the system issues time-limited challenge tokens. The User.isValidChallengeToken() method verifies these.


Middleware Stack

Located in app/Http/Middleware/:

MiddlewarePurpose
JwtAuthenticateValidates JWT bearer token and sets authenticated user
SetTenantDatabaseSwitches MongoDB tenant connection based on JWT or request
CheckPermissionCASL-style permission checking against user roles
CapacitorApiKeyValidates API key header for Capacitor (mobile) clients
CheckUserActivityTracks user activity
IpWhitelistRestricts access by IP (used for backend-to-backend calls)
LocaleSets application locale from request
OptionalJwtAuthOptional JWT -- sets user if token present, continues otherwise
SuperAdminRestricts to super-admin users
TokenTypeValidates token type matches expected type
AuthenticateLaravel's built-in authentication
VerifyDeliverooWebhookValidates Deliveroo webhook signatures
VerifyShopifyWebhookValidates Shopify webhook signatures
VerifySquareWebhookValidates Square webhook signatures
VerifyUberEatsWebhookValidates UberEats webhook signatures

Typical Middleware Chain for a Backoffice API Request

CapacitorApiKey -> JwtAuthenticate -> SetTenantDatabase:backoffice -> CheckPermission -> Controller

Typical Middleware Chain for an Online Ordering Request

SetTenantDatabase:online-ordering -> Controller

Key Services by Domain

BackOffice Domain (app/Services/BackOffice/)

Core CRUD services for merchant management:

ServicePurpose
CategoryServiceCategory CRUD
ContentServiceMedia/content management
CustomerServiceVendor-specific customer management
DeviceServiceKiosk device management
DeviceProfileServiceDevice configuration profiles
DisplayGroupServiceMenu display grouping
InHouseSettingsServiceIn-house/dine-in settings
ItemServiceItem/product CRUD
LoyaltyServiceLoyalty program management
MenuServiceMenu CRUD and availability
ModifierGroupServiceModifier group management
ModifierServiceIndividual modifier management
OfferServicePromotional offer management
OnlineOrderingServiceOnline ordering configuration
OnlineSettingsServiceOnline platform settings
OrderingChannelServiceChannel (kiosk, online, QR) configuration
QrOrderingServiceQR/table ordering settings
TableSectionServiceRestaurant table section management
TaxRateServiceTax rate configuration
TransactionServiceTransaction history and reporting
VariantGroupServiceProduct variant management

Settings sub-domain (app/Services/BackOffice/Settings/):

ServicePurpose
ActivityLogServiceActivity/audit log
BrandingProfileServiceVisual branding (colors, logos)
GuidedSetupServiceFirst-time merchant setup wizard
PaymentProfileServicePayment provider configuration
ReceiptSettingServiceReceipt formatting and content
TeamServiceTeam member management
TranslationServiceMulti-language translation management

Third-Party Integration Services

ServicePurpose
DeliverooServiceDeliveroo integration
KassanetIntegrationServiceKassanet POS integration
ShopifyIntegrationServiceShopify product sync
SquareIntegrationServiceSquare POS integration
SquareUpServiceSquare data sync
UberEatsServiceUberEats integration

Payment Domain (app/Services/Payment/)

ServicePurpose
PaymentServicePayment processing orchestration
PaymentCaptureServicePayment capture/settlement
WebhookServicePayment webhook handling

Common/Shared Services (app/Services/Common/)

ServicePurpose
AppUpdateServiceKiosk app version management
CloudflareImageServiceCloudflare Images upload/retrieval
FCMServiceFirebase push notifications
GeocodingServiceAddress geocoding
HendrickxServiceHendrickx supplier integration
MongoDBCacheMongoDB-backed cache implementation
SMSServiceSMS sending (OTP, notifications)
SpreadsheetServiceExcel export generation
StarMicronicsServiceStar Micronics receipt printer integration
TaxRateServiceShared tax rate logic
TimezoneServiceTimezone handling
UserServiceUser account management
VanhoutteServiceVanhoutte supplier integration
VivaWalletServiceViva Wallet payment integration

Other Domain Services

ServicePurpose
AuthServiceAuthentication logic
AuthCustomerServiceCustomer-facing authentication
AuthDeviceServiceKiosk device authentication
JwtServiceJWT token issuance and validation
PasskeyServiceWebAuthn/passkey operations
PermissionServiceRole-based permission checking
TransactionServiceTransaction processing
BaseVendorServiceShared vendor-level operations
RepresentativeServiceSales representative management
DescriptionGeneratorServiceAI-powered description generation
RestaurantSuggestionServiceRestaurant discovery/suggestions
TableQrOrderingServiceQR-based table ordering flow
ThirdPartyAuthServiceThird-party auth (OAuth) handling

RawModels Architecture

The backend uses a custom RawModels pattern instead of Eloquent ORM models. All models extend BaseModel and are plain PHP objects with typed constructor properties.

Key characteristics:

  • Immutable by default -- Properties are set via constructor and accessed via getters
  • No Eloquent dependency -- Models are decoupled from the ORM
  • Typed properties -- Full PHP type declarations on all fields
  • Connection constants -- Each model declares its CONNECTION and COLLECTION
  • Soft deletes -- Controlled via SOFT_DELETE constant
  • Archiving -- Some models support ARCHIVE constant for archival

BaseModel provides:

  • MongoDB ObjectId management (getId(), getObjectId())
  • Timestamp handling (getCreatedAt(), getUpdatedAt(), getDeletedAt())
  • Carbon date helpers
  • Soft delete checking (isTrashed())
  • ObjectId array conversion (toObjectIds())
  • New instance detection (isNewlyCreated())

Polymorphic Collections

Some MongoDB collections store multiple model types using a model discriminator field:

  • menus collection stores: Item, Menu, Modifier, ModifierGroup (each has MODEL = 'item'|'menu'|'modifier'|'modifier_group')
  • settings collection stores: Category, Language, DisplayGroup, and other settings entities

Background Job System

The backend uses Laravel's queue system with Redis as the driver.

Queue Configuration

  • Driver: Redis (Predis client)
  • Failed jobs: Stored in MongoDB via custom MongoDBFailedJobProvider (registered in AppServiceProvider)

Key Job Types

Jobs are dispatched for operations that should not block the HTTP request:

  • Payment webhook processing
  • Third-party sync operations (Square, Deliveroo, UberEats)
  • FCM push notification sending
  • Email/SMS dispatch
  • Data repair operations (Console/Commands/DataRepair/)

Service Providers

Located in app/Providers/:

ProviderPurpose
AppServiceProviderRegisters custom validation rules and MongoDB failed job provider
HealthServiceProviderRegisters health check implementations
SentryServiceProviderConfigures Sentry error tracking
SessionServiceProviderCustom session handling
StarMicronicsProviderStar Micronics printer SDK registration

Custom Validation Rules

AppServiceProvider registers several cross-database validation rules:

  • exists_in_connection -- Validates a value exists in a specific MongoDB connection/collection
  • exists_in_connection_with_model -- Same, with model type discrimination
  • unique_in_connection_with_model -- Uniqueness check across connections
  • unique_in_app_database_with_model -- Uniqueness in the app (central) database
  • exists_in_connection_array_with_model -- Array field existence check

Route Organization

API Routes (routes/api.php and routes/api/)

Routes are organized by consumer type:

  • routes/api/backoffice/ -- Backoffice dashboard API routes
  • routes/api/guest.php -- Unauthenticated/public endpoints
  • routes/api.php -- Main route file that includes all sub-route files

Each route group applies appropriate middleware for its consumer type (authentication, tenant switching, permissions).


Key Patterns for AI Bug Fixing

When investigating or fixing bugs, keep these patterns in mind:

  1. Always check which database connection a model uses. Central (mongodb) vs tenant (tenant) changes where data is stored and queried.

  2. Multi-tenant context is critical. Ensure the tenant database is set before any tenant-connection query. Missing tenant context causes "database not set" errors or cross-tenant data leaks.

  3. The polymorphic menus and settings collections mean multiple model types share a collection. Always filter by model type when querying.

  4. Services should not call other services directly for complex flows. If a bug involves cross-service coordination, check the relevant Orchestrator.

  5. Factory defaults matter. When creating new records, check app/RawFactories/ for the factory class to understand default field values.

  6. Webhook idempotency. Transaction processing uses idempotency_key to prevent duplicate processing. Webhook orchestrators should always check for duplicates.

  7. Price calculations involve tax rates. The PriceTrait handles tax-inclusive and tax-exclusive pricing per dining option (dine-in, takeout, delivery). Bugs in pricing often involve incorrect tax rate resolution.