Skip to content

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

AppRepositoryUI LibraryState ManagementTarget UserProduction status
Backofficeupvendo-backofficeVuetify 3Vuex 4Merchant staffLive
Kioskupvendo-kioskTailwind CSSPiniaEnd customers (self-service)Live
Online Orderingzestidoo-online-orderingTailwind CSSPiniaEnd customers (web)Live
POSupvendo-posTailwind CSSPiniaStaff on an iPad registerBackend deployed; selectable by test-flagged merchants only (config/pos-providers.php upvendo staged 'test_only' => true)
KDSupvendo-kdsTailwind CSSPiniaKitchen staff on a tabletBackend live; device type offered in the back office to first-party-POS merchants only
Fieldupvendo-fieldTailwind CSSPiniaField 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 on upvendo-backend origin/production -- routes/api/pos.php is required unconditionally at routes/api.php line 239 and app/Http/Controllers/Api/Pos/ holds eight controllers. In production only test-flagged merchants can be onboarded onto it: config/pos-providers.php:51-52 stages upvendo as 'active' => true, 'test_only' => true; the test_only filter (checked before the first_party bypass) hides it from live merchants in ResellerService::getMerchantResellerProviders(), and FirstPartyPosProvisioningService::selectFirstPartyPos() 403s a live merchant. Without the pos_provider='upvendo' marker the back office will not offer the POS device type, so no POS device JWT can be minted. (Verified on origin/production 500b6b998; see POS App.)
  • KDS (upvendo-kds) -- Kitchen Display System, a Vue 3 + Capacitor rewrite of the legacy Flutter upvendo_kds. Runs on Elo (Android) and iPad (iOS) tablets. Its backend is on production -- the /kds/* route group behind type:kds + tenant:kds middleware -- 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-backend routes/api.php line 241 opens the /kds group; upvendo-backoffice/src/constants.ts line 24 lists Kitchen Display unconditionally and line 57 declares FIRST_PARTY_POS_REQUIRED_DEVICE_TYPES = ['POS', 'Kitchen Display'], consumed at src/views/devices/components/dialogs/SelectDeviceTypeDialog.vue lines 44-50; server-side at upvendo-backend/app/Services/BackOffice/DeviceService.php lines 749-761, which is in store() 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-backend routes/api.php line 41 requires routes/api/field.php, and app/Http/Controllers/Api/Field/ exists on origin/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

ComponentTechnology
FrameworkVue 3 (Composition API + Options API mix)
UI LibraryVuetify 3 (Material Design)
State ManagementVuex 4 (modular stores)
RoutingVue Router 4
HTTP ClientAxios
PermissionsCASL (Attribute-Based Access Control)
Mobile WrapperCapacitor (iOS/Android)
Build ToolVite

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 management
  • location -- Active location selection
  • menu -- Menu management
  • item -- Item/product management
  • category -- Category management
  • displayGroup -- Display group management
  • modifier -- Modifier and modifier group management
  • transaction -- Transaction/order data
  • customer -- Customer management
  • device / deviceProfile -- Device management
  • setting -- Application settings (singular)
  • loyalty -- Loyalty program configuration
  • offers -- 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:

  1. User logs in -> JWT token received
  2. Permissions fetched from backend based on user roles
  3. CASL ability instance created with permission rules
  4. Components use v-if="can('update', 'Item')" to conditionally show/hide UI
  5. 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-Key header 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

ComponentTechnology
FrameworkVue 3 (Composition API)
UI LibraryTailwind CSS
State ManagementPinia
RoutingVue Router 4
HTTP ClientCustom FetchInstance wrapper over native fetch -- not Axios
Mobile WrapperCapacitor (Android primarily)
Device SDKCustom device SDK integration
Push NotificationsFirebase Cloud Messaging
Build ToolVite

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:

ExportFileRole
useAppStoreapp.ts (line 61)Navigation/page state, active menu + display groups, language, tags, loading and image-preload state
useAuthStoreauth.ts (line 59)Device token, shop details, branding CSS variables, Firebase payment-status listener
useCustomerStorecustomer.ts (line 18)Customer identification (email / phone / member code / scan)
useDisplayGroupStoredisplayGroup.ts (line 12)Render-memoisation cache for display groups
useFilterStorefilter.ts (line 17)Allergen / dietary-preference / dietary-supplement filtering
useGiftCardStoregiftCard.ts (line 17)Gift-card validation and applied balances
useImageStoreimage.ts (line 17)LRU image cache (300 entries) + offline flag
useItemStoreitem.ts (line 13)Render-memoisation cache for items
useLoyaltyStoreloyalty.ts (line 18)Customer loyalty points and rewards
useModifierStoremodifier.ts (line 13)Render-memoisation cache for modifier groups and modifiers
useOfferStoreoffer.ts (line 154)Offer fetching, qualification progress, validation
usePricingStorepricing.ts (line 40)Mplus determinePricing quotes with fallback to base prices
useProcessingStoreprocessing.ts (line 4)Payment-processing screen state (setup-style store)
useReferenceDataStorereferenceData.ts (line 16)Allergens, dietary preferences/supplements, ingredients from the Item Database API
useInfiniteScrollStorescroller.ts (line 13)Infinite-scroll windowing -- note the file is scroller.ts, and the Pinia id is infiniteScroll
useSearchStoresearch.ts (line 8)Debounced menu search
useShoppingBagStoreshoppingBag.ts (line 15)Not the cart -- a render-memoisation cache for bag rows (item / modifier-group / modifier / price caches)
useToastStoretoast.ts (line 10)Toast notifications
useTransactionStoretransaction.ts (line 109)The cart lives here (shoppingBag array) plus dining option, offers/rewards, tips, stock cache, idempotency key, submit, receipt printing
useUpsellStoreupsell.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, or useMerchantStore in the kiosk. Cart state is on useTransactionStore; menu state is on useAppStore; stock is cached on useTransactionStore.stockCache. useShoppingBagStore exists but is a memoisation cache, not the cart. (Verified: none of those names resolve anywhere under upvendo-kiosk/src/ on origin/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 suppresses click events on long presses, so the kiosk app replaces @click with 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 in useProcessingStore. 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:

  1. /api/* -- Standard backend API calls for authentication, menu data, order submission
  2. /d1-api/* -- Direct D1 calls for real-time stock checking and reservation

Stock flow:

  1. Menu loads -> stock levels fetched from D1 for all items
  2. Customer adds item to cart -> reservation created in D1
  3. Customer completes order -> reservations released, stock decremented
  4. 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

ComponentTechnology
FrameworkVue 3 (Composition API)
UI LibraryTailwind CSS
State ManagementPinia
RoutingVue Router 4
HTTP ClientCustom FetchInstance wrapper over native fetch -- not Axios
Build ToolVite

Architecture Patterns

Multi-Merchant Routing: The online ordering app serves multiple merchants from a single deployment. Merchant identification works through:

  1. Slug-based routing: URLs contain the merchant slug (e.g., /acme-restaurant/menu)
  2. Domain-based routing: Custom domains resolve to specific merchants
  3. Tenant resolution: the backend's App\Http\Middleware\SetTenantDatabase -- aliased as tenant, applied to the route group as tenant:online-ordering -- uses the slug to find the correct tenant database. (Verified: upvendo-backend/bootstrap/app.php line 33 registers the alias; routes/api.php line 44 applies tenant:online-ordering; app/Http/Middleware/SetTenantDatabase.php line 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:

ExportFileRole
useAddressStoreaddresses.ts (line 15)Saved delivery addresses -- Pinia id is address, singular
useAppStoreapp.ts (line 195)Merchant + location resolution, branding, menus, business hours, page state
useAuthStoreauth.ts (line 27)Phone/OTP customer authentication
useEmilyStoreemily.ts (line 52)Emily ordering-assistant conversation state
useErrorStoreerror.ts (line 5)Central error surface with a suppressed-message list
useFilterStorefilter.ts (line 17)Allergen / dietary filtering
useGiftCardStoregiftCard.ts (line 31)Gift-card validation and purchases
useLocalOrderHistoryStorelocalOrderHistory.ts (line 27)Guest order numbers kept in localStorage
useLoyaltyStoreloyalty.ts (line 15)Loyalty points and rewards
useOfferStoreoffer.ts (line 153)Offers, qualification progress, validation
useOrderHistoryStoreorderHistory.ts (line 19)Server-side order history and detail
usePricingStorepricing.ts (line 36)Mplus determinePricing quotes
useProfileStoreprofile.ts (line 17)Authenticated customer profile
useReferenceDataStorereferenceData.ts (line 16)Allergens/dietary/ingredients from the Item Database API
useRestaurantStorerestaurant.ts (line 18)Nearby-restaurant discovery -- Pinia id is restaurants, plural
useSearchStoresearch.ts (line 10)Menu search
useSidebarStoresidebar.ts (line 48)Sidebar open/view state
useToastStoretoast.ts (line 26)Toast notifications
useTransactionStoretransaction.ts (line 318)The cart lives here (shoppingBag array) plus dining option, address, timeslot, tip, checkout form, idempotency key, submit
useUpsellStoreupsell.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, and usePaymentStore do not exist. Cart state is on useTransactionStore; merchant, location, branding and menu state are all on useAppStore; the customer profile is useProfileStore and customer auth is useAuthStore. (Verified: the only occurrence of useCartStore anywhere under zestidoo-online-ordering/src/ on origin/production is a comment in src/components/emily/EmilyProductCard.vue line 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:

  1. Customer arrives at merchant page (via slug or custom domain)
  2. Location selection (if merchant has multiple locations)
  3. Menu browsing with real-time availability
  4. Cart building with modifier selection
  5. Dining option selection (delivery / pickup)
  6. Customer authentication (login or guest checkout)
  7. Delivery address entry (for delivery orders)
  8. Payment via Stripe
  9. 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:

DomainCountry
zestidoo.comGlobal
zestidoo.beBelgium
zestidoo.nlNetherlands
zestidoo.frFrance
zestidoo.deGermany
zestidoo.co.ukUK

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:

  1. A single client instance with base URL pointing to the Cloudflare Workers proxy. The backoffice uses Axios (axios is a real dependency there). The kiosk and Online Ordering both use a hand-rolled FetchInstance class in src/api/instances/fetchInstance.ts -- native fetch behind an Axios-shaped surface (defaults.headers.common, create(), interceptor-like hooks). Neither repo has axios in package.json; the only occurrences of the string are comments inside fetchInstance.ts. (Verified: upvendo-kiosk/src/api/instances/fetchInstance.ts -- class FetchInstance, this.baseURL = import.meta.env.VITE_BACKEND_API_ENDPOINT at line 434; same file exists in zestidoo-online-ordering with baseURL at line 252.)
  2. Request interceptors for adding authentication headers
  3. Response interceptors for handling 401 (unauthorized) responses
  4. 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 details object 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.ts line 434; zestidoo-online-ordering/src/api/instances/fetchInstance.ts line 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.ts line 307 and src/api/instances/fetchInstance.ts line 15; zestidoo-online-ordering/src/utils/helpers.ts line 230.)
  • Environment-specific .env files 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 the defineStore export. In particular there is no useCartStore in either app; the cart is on useTransactionStore in both.

  1. 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).

  2. 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.

  3. 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.

  4. 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-ordering middleware (App\Http\Middleware\SetTenantDatabase).

  5. Branding not loading usually means the BrandingProfile is missing or the Cloudflare Image ID is invalid. Check the vendor's branding_profile_id and the tenant database.

  6. Payment flow failures span multiple systems (frontend store -> proxy -> backend -> Stripe/Viva). Trace the flow from the useTransactionStore submit action through to the backend webhook handler. Neither the kiosk nor Online Ordering has a separate payment store.

  7. 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.