Appearance
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
| Component | Technology |
|---|---|
| Framework | Laravel 11 (PHP 8.2+) |
| Database | MongoDB (via mongodb/laravel-mongodb) |
| Cache / Queue | Redis (Predis client) |
| Authentication | Custom JWT + OTP + Passkey (WebAuthn) |
| Image Storage | Cloudflare Images |
| SMS | SMS service (for OTP) |
| Push Notifications | Firebase Cloud Messaging (FCM) |
| Payment Providers | Stripe, Viva Wallet |
| Third-Party POS | Square, Kassanet, Hendrickx, Vanhoutte, Shopify |
| Delivery | UberEats, Deliveroo |
| Monitoring | Sentry, 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 servicesService-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.phpRequest-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:
| Connection | Purpose |
|---|---|
mongodb | Central/shared database (vendors, users, locations, roles) |
tenant | Per-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
- JWT token contains
tenant_databasein its payload. - SetTenantDatabase middleware extracts the tenant database name and calls
setTenantDatabase()to configure thetenantconnection. - All subsequent queries on the
tenantconnection target that vendor's database.
The middleware handles three routing strategies:
| Type | Strategy |
|---|---|
backoffice | Extract tenant_database from JWT payload |
kiosk / kds | Extract tenant_database from JWT payload |
online-ordering | Look up tenant from request slug parameter |
customer | Look up tenant from request locationId parameter |
Data Separation
Central database (mongodb connection):
users-- Backoffice user accountsvendors-- Merchant/vendor records (containsdatabase_namefield)locations-- Physical location recordsroles-- Permission rolespayment_profiles-- Payment configurationcustomers-- 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 recordstransaction_items-- Line items within transactionsinventories-- Stock/inventory trackingbranding_profiles-- Visual branding per tenantbilling_profiles-- Billing configurationcustomer_vendors-- Vendor-specific customer dataloyalty-- Loyalty program configurationoffers-- Promotional offersdevices-- Registered kiosk devicesdevice_profiles-- Device configuration profilessubscriptions-- 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 codelast_otp_request-- Timestamp of last OTP requestresend_counter-- Rate limiting counter
Passkey / WebAuthn
Modern passwordless authentication. The User model stores:
webauthn_challenge-- Current challenge for registration/authenticationpasskeys-- 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/:
| Middleware | Purpose |
|---|---|
JwtAuthenticate | Validates JWT bearer token and sets authenticated user |
SetTenantDatabase | Switches MongoDB tenant connection based on JWT or request |
CheckPermission | CASL-style permission checking against user roles |
CapacitorApiKey | Validates API key header for Capacitor (mobile) clients |
CheckUserActivity | Tracks user activity |
IpWhitelist | Restricts access by IP (used for backend-to-backend calls) |
Locale | Sets application locale from request |
OptionalJwtAuth | Optional JWT -- sets user if token present, continues otherwise |
SuperAdmin | Restricts to super-admin users |
TokenType | Validates token type matches expected type |
Authenticate | Laravel's built-in authentication |
VerifyDeliverooWebhook | Validates Deliveroo webhook signatures |
VerifyShopifyWebhook | Validates Shopify webhook signatures |
VerifySquareWebhook | Validates Square webhook signatures |
VerifyUberEatsWebhook | Validates UberEats webhook signatures |
Typical Middleware Chain for a Backoffice API Request
CapacitorApiKey -> JwtAuthenticate -> SetTenantDatabase:backoffice -> CheckPermission -> ControllerTypical Middleware Chain for an Online Ordering Request
SetTenantDatabase:online-ordering -> ControllerKey Services by Domain
BackOffice Domain (app/Services/BackOffice/)
Core CRUD services for merchant management:
| Service | Purpose |
|---|---|
CategoryService | Category CRUD |
ContentService | Media/content management |
CustomerService | Vendor-specific customer management |
DeviceService | Kiosk device management |
DeviceProfileService | Device configuration profiles |
DisplayGroupService | Menu display grouping |
InHouseSettingsService | In-house/dine-in settings |
ItemService | Item/product CRUD |
LoyaltyService | Loyalty program management |
MenuService | Menu CRUD and availability |
ModifierGroupService | Modifier group management |
ModifierService | Individual modifier management |
OfferService | Promotional offer management |
OnlineOrderingService | Online ordering configuration |
OnlineSettingsService | Online platform settings |
OrderingChannelService | Channel (kiosk, online, QR) configuration |
QrOrderingService | QR/table ordering settings |
TableSectionService | Restaurant table section management |
TaxRateService | Tax rate configuration |
TransactionService | Transaction history and reporting |
VariantGroupService | Product variant management |
Settings sub-domain (app/Services/BackOffice/Settings/):
| Service | Purpose |
|---|---|
ActivityLogService | Activity/audit log |
BrandingProfileService | Visual branding (colors, logos) |
GuidedSetupService | First-time merchant setup wizard |
PaymentProfileService | Payment provider configuration |
ReceiptSettingService | Receipt formatting and content |
TeamService | Team member management |
TranslationService | Multi-language translation management |
Third-Party Integration Services
| Service | Purpose |
|---|---|
DeliverooService | Deliveroo integration |
KassanetIntegrationService | Kassanet POS integration |
ShopifyIntegrationService | Shopify product sync |
SquareIntegrationService | Square POS integration |
SquareUpService | Square data sync |
UberEatsService | UberEats integration |
Payment Domain (app/Services/Payment/)
| Service | Purpose |
|---|---|
PaymentService | Payment processing orchestration |
PaymentCaptureService | Payment capture/settlement |
WebhookService | Payment webhook handling |
Common/Shared Services (app/Services/Common/)
| Service | Purpose |
|---|---|
AppUpdateService | Kiosk app version management |
CloudflareImageService | Cloudflare Images upload/retrieval |
FCMService | Firebase push notifications |
GeocodingService | Address geocoding |
HendrickxService | Hendrickx supplier integration |
MongoDBCache | MongoDB-backed cache implementation |
SMSService | SMS sending (OTP, notifications) |
SpreadsheetService | Excel export generation |
StarMicronicsService | Star Micronics receipt printer integration |
TaxRateService | Shared tax rate logic |
TimezoneService | Timezone handling |
UserService | User account management |
VanhoutteService | Vanhoutte supplier integration |
VivaWalletService | Viva Wallet payment integration |
Other Domain Services
| Service | Purpose |
|---|---|
AuthService | Authentication logic |
AuthCustomerService | Customer-facing authentication |
AuthDeviceService | Kiosk device authentication |
JwtService | JWT token issuance and validation |
PasskeyService | WebAuthn/passkey operations |
PermissionService | Role-based permission checking |
TransactionService | Transaction processing |
BaseVendorService | Shared vendor-level operations |
RepresentativeService | Sales representative management |
DescriptionGeneratorService | AI-powered description generation |
RestaurantSuggestionService | Restaurant discovery/suggestions |
TableQrOrderingService | QR-based table ordering flow |
ThirdPartyAuthService | Third-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
CONNECTIONandCOLLECTION - Soft deletes -- Controlled via
SOFT_DELETEconstant - Archiving -- Some models support
ARCHIVEconstant 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:
menuscollection stores: Item, Menu, Modifier, ModifierGroup (each hasMODEL = 'item'|'menu'|'modifier'|'modifier_group')settingscollection 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 inAppServiceProvider)
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/:
| Provider | Purpose |
|---|---|
AppServiceProvider | Registers custom validation rules and MongoDB failed job provider |
HealthServiceProvider | Registers health check implementations |
SentryServiceProvider | Configures Sentry error tracking |
SessionServiceProvider | Custom session handling |
StarMicronicsProvider | Star 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/collectionexists_in_connection_with_model-- Same, with model type discriminationunique_in_connection_with_model-- Uniqueness check across connectionsunique_in_app_database_with_model-- Uniqueness in the app (central) databaseexists_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 routesroutes/api/guest.php-- Unauthenticated/public endpointsroutes/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:
Always check which database connection a model uses. Central (
mongodb) vs tenant (tenant) changes where data is stored and queried.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.
The polymorphic
menusandsettingscollections mean multiple model types share a collection. Always filter bymodeltype when querying.Services should not call other services directly for complex flows. If a bug involves cross-service coordination, check the relevant Orchestrator.
Factory defaults matter. When creating new records, check
app/RawFactories/for the factory class to understand default field values.Webhook idempotency. Transaction processing uses
idempotency_keyto prevent duplicate processing. Webhook orchestrators should always check for duplicates.Price calculations involve tax rates. The
PriceTraithandles tax-inclusive and tax-exclusive pricing per dining option (dine-in, takeout, delivery). Bugs in pricing often involve incorrect tax rate resolution.