Appearance
Frontend Applications Overview
Upvendo has six first-party Vue 3 frontend applications, each targeting a different user persona and use case. They all communicate with the backend through the Cloudflare Workers proxy.
Three of them -- Backoffice, Kiosk, and Online Ordering -- are the long-standing apps and are documented in depth below. POS, KDS, and Field are newer additions and are only summarised here.
Application Summary
| App | Repository | UI Library | State Management | Target User | Production status |
|---|---|---|---|---|---|
| Backoffice | upvendo-backoffice | Vuetify 3 | Vuex 4 | Merchant staff | Live |
| Kiosk | upvendo-kiosk | Tailwind CSS | Pinia | End customers (self-service) | Live |
| Online Ordering | zestidoo-online-ordering | Tailwind CSS | Pinia | End customers (web) | Live |
| POS | upvendo-pos | Tailwind CSS | Pinia | Staff on an iPad register | Backend deployed; selectable by test-flagged merchants only (config/pos-providers.php upvendo staged 'test_only' => true) |
| KDS | upvendo-kds | Tailwind CSS | Pinia | Kitchen staff on a tablet | Backend live; device type offered in the back office to first-party-POS merchants only |
| Field | upvendo-field | Tailwind CSS | Pinia | Field reps ("placers") | Live |
All six are Vue 3 + Vite, and all six carry a capacitor.config for native packaging. (Verified: package.json name field plus the Vuetify/Tailwind and Vuex/Pinia dependencies in each repo on origin/production; capacitor.config.* present at the root of all six.)
The Online Ordering repository is zestidoo-online-ordering, not upvendo-online-ordering. (Verified: zestidoo-online-ordering/package.json line 2.)
The newer three
- POS (
upvendo-pos) -- staff-operated point of sale, iPad landscape-first: device activation, staff PIN login, register, fire to kitchen, cash settle. Its backend IS onupvendo-backendorigin/production--routes/api/pos.phpis required unconditionally atroutes/api.phpline 239 andapp/Http/Controllers/Api/Pos/holds eight controllers. In production only test-flagged merchants can be onboarded onto it:config/pos-providers.php:51-52stagesupvendoas'active' => true, 'test_only' => true; thetest_onlyfilter (checked before thefirst_partybypass) hides it from live merchants inResellerService::getMerchantResellerProviders(), andFirstPartyPosProvisioningService::selectFirstPartyPos()403s a live merchant. Without thepos_provider='upvendo'marker the back office will not offer the POS device type, so no POS device JWT can be minted. (Verified onorigin/production500b6b998; see POS App.) - KDS (
upvendo-kds) -- Kitchen Display System, a Vue 3 + Capacitor rewrite of the legacy Flutterupvendo_kds. Runs on Elo (Android) and iPad (iOS) tablets. Its backend is on production -- the/kds/*route group behindtype:kds+tenant:kdsmiddleware -- and the "Kitchen Display" device type is offered in the back office -- but both the tile and the create endpoint are gated on the merchant running the first-party Upvendo POS. The tile is rendered and disabled with a tooltip; existing KDS devices on other providers are grandfathered because only device creation is gated. (Verified:upvendo-kds/README.md;upvendo-backendroutes/api.phpline 241 opens the/kdsgroup;upvendo-backoffice/src/constants.tsline 24 listsKitchen Displayunconditionally and line 57 declaresFIRST_PARTY_POS_REQUIRED_DEVICE_TYPES = ['POS', 'Kitchen Display'], consumed atsrc/views/devices/components/dialogs/SelectDeviceTypeDialog.vuelines 44-50; server-side atupvendo-backend/app/Services/BackOffice/DeviceService.phplines 749-761, which is instore()only --update()has no equivalent check.) - Field (
upvendo-field) -- mobile-first PWA (plus native iOS/Android via Capacitor) for the NFC Review-Stand Field Ops program: scan a stand code, pick the business, take a photo, activate the stand. Its backend is live at/field/*. (Verified:upvendo-field/CLAUDE.md;upvendo-backendroutes/api.phpline 41 requiresroutes/api/field.php, andapp/Http/Controllers/Api/Field/exists onorigin/production.)
All three of these origins are in the proxy's production ALLOWED_ORIGINS (https://pos.upvendo.com, https://kds.upvendo.com, https://field.upvendo.com). (Verified: upvendo-backend-proxy/wrangler.toml line 54.)
Backoffice Application
Purpose: The merchant management dashboard. Used by business owners, managers, and staff to configure their restaurants, manage menus, view transactions, handle customers, and manage all aspects of their Upvendo setup.
Technology Stack
| Component | Technology |
|---|---|
| Framework | Vue 3 (Composition API + Options API mix) |
| UI Library | Vuetify 3 (Material Design) |
| State Management | Vuex 4 (modular stores) |
| Routing | Vue Router 4 |
| HTTP Client | Axios |
| Permissions | CASL (Attribute-Based Access Control) |
| Mobile Wrapper | Capacitor (iOS/Android) |
| Build Tool | Vite |
Architecture Patterns
Vuex Modular Store: The Vuex store is split into domain-specific modules that mirror the backend service structure. There are 62 of them under src/store/modules/, registered through direct-vuex's createDirectStore(). A representative subset:
auth-- Authentication state, JWT token managementlocation-- Active location selectionmenu-- Menu managementitem-- Item/product managementcategory-- Category managementdisplayGroup-- Display group managementmodifier-- Modifier and modifier group managementtransaction-- Transaction/order datacustomer-- Customer managementdevice/deviceProfile-- Device managementsetting-- Application settings (singular)loyalty-- Loyalty program configurationoffers-- Offer management (plural)
There is no vendor module. Note the exact names: setting is singular and offers is plural. Read src/store/index.ts for the full registration list before assuming a module exists. (Verified: upvendo-backoffice/src/store/index.ts lines 1-58 and the 62 files under src/store/modules/ on origin/production.)
Each module typically includes: state, mutations, actions (for API calls), and getters.
Pinia is also installed in the backoffice, but only for the two Vuexy-template config stores (src/@core/stores/config.ts and src/@layouts/stores/config.ts). Application state is Vuex. (Verified: defineStore appears in exactly those two files on origin/production.)
CASL Permissions: The backoffice uses CASL for fine-grained permission checking. Permissions are loaded from the backend based on the user's roles and define what actions (create, read, update, delete) are allowed on which resources.
Permissions flow:
- User logs in -> JWT token received
- Permissions fetched from backend based on user roles
- CASL ability instance created with permission rules
- Components use
v-if="can('update', 'Item')"to conditionally show/hide UI - Route guards check permissions before navigation
Multi-Location Support: The backoffice supports multi-location merchants. The active location is stored in state and many API calls include a location_id parameter. Location switching updates the store and refreshes data.
Capacitor Mobile Wrapper: The backoffice is wrapped in Capacitor for iOS and Android deployment. The CapacitorApiKey header is sent with API requests for device identification. Native features accessed through Capacitor include:
- Push notifications (Firebase)
- Camera access (for photo studio)
- Biometric authentication
- App update management
API Integration
All API calls go through an Axios instance configured with:
- Base URL pointing to the Cloudflare Workers proxy
- JWT bearer token in Authorization header
X-Capacitor-API-Keyheader for mobile clients- Response interceptors for 401 handling (token refresh or logout)
- Request interceptors for adding tenant context
Key Features
- Menu and item management (CRUD, drag-and-drop ordering)
- Transaction history and reporting
- Customer management and loyalty programs
- Device and kiosk management
- Branding and visual customization
- Multi-language content management
- Third-party integration configuration (Square, Deliveroo, UberEats, Shopify)
- Photo studio for item images
- Team member management with role-based permissions
- Tax rate and billing configuration
- Guided setup wizard for new merchants
Kiosk Application
Purpose: Self-service ordering application running on kiosk hardware (tablets/touchscreens) in physical restaurant locations. Customers use it to browse the menu, customize orders, and pay.
Technology Stack
| Component | Technology |
|---|---|
| Framework | Vue 3 (Composition API) |
| UI Library | Tailwind CSS |
| State Management | Pinia |
| Routing | Vue Router 4 |
| HTTP Client | Custom FetchInstance wrapper over native fetch -- not Axios |
| Mobile Wrapper | Capacitor (Android primarily) |
| Device SDK | Custom device SDK integration |
| Push Notifications | Firebase Cloud Messaging |
| Build Tool | Vite |
Architecture Patterns
Pinia Stores: The kiosk uses Pinia for state management. Every store lives in src/stores/ and is exported from the file named after it. The complete list on origin/production:
| Export | File | Role |
|---|---|---|
useAppStore | app.ts (line 61) | Navigation/page state, active menu + display groups, language, tags, loading and image-preload state |
useAuthStore | auth.ts (line 59) | Device token, shop details, branding CSS variables, Firebase payment-status listener |
useCustomerStore | customer.ts (line 18) | Customer identification (email / phone / member code / scan) |
useDisplayGroupStore | displayGroup.ts (line 12) | Render-memoisation cache for display groups |
useFilterStore | filter.ts (line 17) | Allergen / dietary-preference / dietary-supplement filtering |
useGiftCardStore | giftCard.ts (line 17) | Gift-card validation and applied balances |
useImageStore | image.ts (line 17) | LRU image cache (300 entries) + offline flag |
useItemStore | item.ts (line 13) | Render-memoisation cache for items |
useLoyaltyStore | loyalty.ts (line 18) | Customer loyalty points and rewards |
useModifierStore | modifier.ts (line 13) | Render-memoisation cache for modifier groups and modifiers |
useOfferStore | offer.ts (line 154) | Offer fetching, qualification progress, validation |
usePricingStore | pricing.ts (line 40) | Mplus determinePricing quotes with fallback to base prices |
useProcessingStore | processing.ts (line 4) | Payment-processing screen state (setup-style store) |
useReferenceDataStore | referenceData.ts (line 16) | Allergens, dietary preferences/supplements, ingredients from the Item Database API |
useInfiniteScrollStore | scroller.ts (line 13) | Infinite-scroll windowing -- note the file is scroller.ts, and the Pinia id is infiniteScroll |
useSearchStore | search.ts (line 8) | Debounced menu search |
useShoppingBagStore | shoppingBag.ts (line 15) | Not the cart -- a render-memoisation cache for bag rows (item / modifier-group / modifier / price caches) |
useToastStore | toast.ts (line 10) | Toast notifications |
useTransactionStore | transaction.ts (line 109) | The cart lives here (shoppingBag array) plus dining option, offers/rewards, tips, stock cache, idempotency key, submit, receipt printing |
useUpsellStore | upsell.ts (line 32) | Upsell groups |
(Verified: defineStore export in each of the files listed, upvendo-kiosk/src/stores/*.ts on origin/production. The kiosk cart array is transaction.ts line 67, shoppingBag: [] as any[]; the stock cache is line 69.)
Trap. There is no
useCartStore,useMenuStore,useLocationStore,useSettingsStore,usePaymentStore,useOrderStore,useStockStore, oruseMerchantStorein the kiosk. Cart state is onuseTransactionStore; menu state is onuseAppStore; stock is cached onuseTransactionStore.stockCache.useShoppingBagStoreexists but is a memoisation cache, not the cart. (Verified: none of those names resolve anywhere underupvendo-kiosk/src/onorigin/production.)
Device-First Design: The kiosk is designed for touchscreen interaction:
- Large touch targets
- No keyboard input (on-screen keyboard for customer info)
- Idle timeout with screensaver/attract mode
- Hardware integration (payment terminals, receipt printers)
- Custom tap recognizer (
v-kiosk-tap) for all interactive elements -- Android WebView suppressesclickevents on long presses, so the kiosk app replaces@clickwith a custom Vue directive that listens to raw touch events. See Kiosk Tap Directive for the full rationale and rules.
Cart Management: useTransactionStore handles:
- Adding/removing items with modifier selections
- Quantity adjustments
- Stock reservation via the D1 proxy (prevents overselling)
- Price calculation including modifiers, taxes, and discounts
- Loyalty reward application
- Dining option selection (for here / takeout)
Payment Flow: Payment integration varies by terminal:
- Stripe Terminal (physical card reader)
- Viva Wallet Terminal
- Terminal communication is orchestrated by
useTransactionStore(submit / cancel / retrieve-after-payment) with the on-screen processing state held inuseProcessingStore. There is no separate payment store.
Firebase Integration: Used for:
- Push notifications for order status updates
- Remote configuration updates
- Analytics
API Integration
The kiosk communicates through two proxy paths:
/api/*-- Standard backend API calls for authentication, menu data, order submission/d1-api/*-- Direct D1 calls for real-time stock checking and reservation
Stock flow:
- Menu loads -> stock levels fetched from D1 for all items
- Customer adds item to cart -> reservation created in D1
- Customer completes order -> reservations released, stock decremented
- Customer abandons cart -> reservations expire after 10 minutes
Key Features
- Full menu browsing with categories and display groups
- Item detail view with allergen/ingredient information
- Modifier selection with pricing
- Variant group selection (e.g., sizes)
- Shopping cart with real-time stock validation
- Loyalty program integration (scan/enter loyalty card)
- Multiple dining options (for here, takeout)
- Integrated payment terminal support
- Receipt printing (CloudPRNT-capable network printers)
- Multi-language support (language selection at start)
- Idle timeout and attract mode
- Accessibility considerations for touchscreen use
Online Ordering Application (Zestidoo)
Purpose: Customer-facing web application for online ordering. Customers access it through custom branded domains (e.g., zestidoo.be/merchant-slug) to place delivery or pickup orders.
Technology Stack
| Component | Technology |
|---|---|
| Framework | Vue 3 (Composition API) |
| UI Library | Tailwind CSS |
| State Management | Pinia |
| Routing | Vue Router 4 |
| HTTP Client | Custom FetchInstance wrapper over native fetch -- not Axios |
| Build Tool | Vite |
Architecture Patterns
Multi-Merchant Routing: The online ordering app serves multiple merchants from a single deployment. Merchant identification works through:
- Slug-based routing: URLs contain the merchant slug (e.g.,
/acme-restaurant/menu) - Domain-based routing: Custom domains resolve to specific merchants
- Tenant resolution: the backend's
App\Http\Middleware\SetTenantDatabase-- aliased astenant, applied to the route group astenant:online-ordering-- uses the slug to find the correct tenant database. (Verified:upvendo-backend/bootstrap/app.phpline 33 registers the alias;routes/api.phpline 44 appliestenant:online-ordering;app/Http/Middleware/SetTenantDatabase.phpline 39 branches on$type === 'online-ordering'.)
Pinia Stores: every store lives in src/stores/ and is exported from the file named after it. The complete list on origin/production:
| Export | File | Role |
|---|---|---|
useAddressStore | addresses.ts (line 15) | Saved delivery addresses -- Pinia id is address, singular |
useAppStore | app.ts (line 195) | Merchant + location resolution, branding, menus, business hours, page state |
useAuthStore | auth.ts (line 27) | Phone/OTP customer authentication |
useEmilyStore | emily.ts (line 52) | Emily ordering-assistant conversation state |
useErrorStore | error.ts (line 5) | Central error surface with a suppressed-message list |
useFilterStore | filter.ts (line 17) | Allergen / dietary filtering |
useGiftCardStore | giftCard.ts (line 31) | Gift-card validation and purchases |
useLocalOrderHistoryStore | localOrderHistory.ts (line 27) | Guest order numbers kept in localStorage |
useLoyaltyStore | loyalty.ts (line 15) | Loyalty points and rewards |
useOfferStore | offer.ts (line 153) | Offers, qualification progress, validation |
useOrderHistoryStore | orderHistory.ts (line 19) | Server-side order history and detail |
usePricingStore | pricing.ts (line 36) | Mplus determinePricing quotes |
useProfileStore | profile.ts (line 17) | Authenticated customer profile |
useReferenceDataStore | referenceData.ts (line 16) | Allergens/dietary/ingredients from the Item Database API |
useRestaurantStore | restaurant.ts (line 18) | Nearby-restaurant discovery -- Pinia id is restaurants, plural |
useSearchStore | search.ts (line 10) | Menu search |
useSidebarStore | sidebar.ts (line 48) | Sidebar open/view state |
useToastStore | toast.ts (line 26) | Toast notifications |
useTransactionStore | transaction.ts (line 318) | The cart lives here (shoppingBag array) plus dining option, address, timeslot, tip, checkout form, idempotency key, submit |
useUpsellStore | upsell.ts (line 39) | Upsell groups |
(Verified: defineStore export in each of the files listed, zestidoo-online-ordering/src/stores/*.ts on origin/production. The cart array is transaction.ts line 258, shoppingBag: [] as any[].)
Trap. Online Ordering has no dedicated cart, menu, merchant, customer, location, order, or payment store.
useCartStore,useMenuStore,useMerchantStore,useCustomerStore,useLocationStore,useOrderStore, andusePaymentStoredo not exist. Cart state is onuseTransactionStore; merchant, location, branding and menu state are all onuseAppStore; the customer profile isuseProfileStoreand customer auth isuseAuthStore. (Verified: the only occurrence ofuseCartStoreanywhere underzestidoo-online-ordering/src/onorigin/productionis a comment insrc/components/emily/EmilyProductCard.vueline 38.)
Branding System: Each merchant has a custom visual identity applied at runtime:
- Primary/secondary colors from BrandingProfile
- Logo and cover images from Cloudflare Images
- Custom fonts (if configured)
- The branding is loaded during merchant resolution and applied as CSS variables
Order Flow:
- Customer arrives at merchant page (via slug or custom domain)
- Location selection (if merchant has multiple locations)
- Menu browsing with real-time availability
- Cart building with modifier selection
- Dining option selection (delivery / pickup)
- Customer authentication (login or guest checkout)
- Delivery address entry (for delivery orders)
- Payment via Stripe
- Order confirmation with real-time status tracking
API Integration
The online ordering app primarily uses /api/* through the proxy. Key differences from backoffice/kiosk API usage:
- No JWT for initial load -- Menu and merchant data is loaded without authentication
- Slug-based tenant resolution -- The merchant slug is passed in the URL, not in a JWT
- Customer authentication -- Separate auth flow (phone + OTP, not email + password)
- Stock via proxy -- Stock availability comes through the D1 proxy for real-time data
Multi-Domain Deployment
The app is deployed to multiple domains per country:
| Domain | Country |
|---|---|
zestidoo.com | Global |
zestidoo.be | Belgium |
zestidoo.nl | Netherlands |
zestidoo.fr | France |
zestidoo.de | Germany |
zestidoo.co.uk | UK |
Each domain serves the same application but may have country-specific defaults (currency, language, etc.).
Key Features
- Merchant landing page with branding
- Location selection with map view
- Full menu browsing with search and categories
- Item detail with allergens, ingredients, dietary info
- Modifier and variant selection
- Shopping cart with subtotal calculation
- Guest checkout or authenticated ordering
- Delivery address management with geocoding
- Pickup time selection
- Stripe online payment
- Order tracking with real-time status updates
- Order history for authenticated customers
- Multi-language support
- Responsive design (mobile-first)
- SEO-friendly rendering
Shared Patterns Across All Frontends
API Layer Architecture
The apps follow the same shape, but not the same HTTP client:
- A single client instance with base URL pointing to the Cloudflare Workers proxy. The backoffice uses Axios (
axiosis a real dependency there). The kiosk and Online Ordering both use a hand-rolledFetchInstanceclass insrc/api/instances/fetchInstance.ts-- nativefetchbehind an Axios-shaped surface (defaults.headers.common,create(), interceptor-like hooks). Neither repo hasaxiosinpackage.json; the only occurrences of the string are comments insidefetchInstance.ts. (Verified:upvendo-kiosk/src/api/instances/fetchInstance.ts--class FetchInstance,this.baseURL = import.meta.env.VITE_BACKEND_API_ENDPOINTat line 434; same file exists inzestidoo-online-orderingwithbaseURLat line 252.) - Request interceptors for adding authentication headers
- Response interceptors for handling 401 (unauthorized) responses
- Service modules that wrap API calls with typed parameters and return values
Multi-Language Support
All apps support multiple languages:
- Language data is fetched from the backend per tenant
- Items, categories, and menus use the
detailsobject with language-keyed names/descriptions - The UI itself uses Vue i18n (or equivalent) for static strings
- Language selection is stored in local state
Branding Integration
Each app loads branding from the backend:
- Colors (primary, secondary, accent)
- Logo images (via Cloudflare Images with variant presets)
- The kiosk and online ordering apps apply branding as CSS custom properties
- The backoffice shows branding previews in the settings UI
Error Handling
Common error handling approach:
- Network errors show toast/snackbar notifications
- Validation errors are mapped to form field errors
- 401 responses trigger re-authentication flow
- 500 errors show generic error messages with retry options
Environment Configuration
All apps use Vite environment variables for configuration:
VITE_BACKEND_API_ENDPOINT-- the proxy base URL. This is the variable the HTTP client actually reads in both the kiosk and Online Ordering. (Verified:upvendo-kiosk/src/api/instances/fetchInstance.tsline 434;zestidoo-online-ordering/src/api/instances/fetchInstance.tsline 252;zestidoo-online-ordering/.env.example.)VITE_CLOUDFLARE_D1_API_URL-- the D1 / Item Database API base on the same proxy.VITE_ENV-- current environment (production,staging,testing,local). (Verified:upvendo-kiosk/src/utils/helpers.tsline 307 andsrc/api/instances/fetchInstance.tsline 15;zestidoo-online-ordering/src/utils/helpers.tsline 230.)- Environment-specific
.envfiles for each deployment target
There is no VITE_ENVIRONMENT variable in any frontend -- the name is VITE_ENV. And VITE_API_BASE_URL is not the runtime API base: in Online Ordering it appears only in the build-time SEO/sitemap generator (process.env.VITE_API_BASE_URL, src/utils/seoMetaGenerator.ts line 95), and it does not appear at all in the kiosk. (Verified: exhaustive git grep -o 'VITE_[A-Z0-9_]*' over src/ on origin/production in both repos.)
Key Patterns for AI Bug Fixing
Never guess a store name. The kiosk and Online Ordering store sets are listed exhaustively above, and they do not follow the obvious naming. Before importing
useSomethingStore, confirm it exists --git -C <repo> ls-tree -r --name-only origin/production | grep 'src/stores/', then read thedefineStoreexport. In particular there is nouseCartStorein either app; the cart is onuseTransactionStorein both.
Check which state management system the app uses. Backoffice uses Vuex (mutations + actions), while Kiosk and Online Ordering use Pinia (direct state mutation in actions).
Permission bugs in the backoffice often relate to CASL ability definitions. Check the permission rules loaded from the backend and the
can()checks in components.Stock discrepancies between what the customer sees and actual availability usually involve the D1 proxy layer. Check if stock reservations are being created/released correctly.
Multi-tenant context in online ordering depends on the slug being correct. If a customer sees the wrong merchant's data, check the slug resolution in the route and the
tenant:online-orderingmiddleware (App\Http\Middleware\SetTenantDatabase).Branding not loading usually means the BrandingProfile is missing or the Cloudflare Image ID is invalid. Check the vendor's
branding_profile_idand the tenant database.Payment flow failures span multiple systems (frontend store -> proxy -> backend -> Stripe/Viva). Trace the flow from the
useTransactionStoresubmit action through to the backend webhook handler. Neither the kiosk nor Online Ordering has a separate payment store.Capacitor-specific bugs in the backoffice only occur on mobile. Check for
Capacitor.isNativePlatform()guards and ensure native plugin calls are wrapped in try/catch.