Skip to content

Proxy Layer Architecture Overview

The Upvendo backend proxy (upvendo-backend-proxy) is a Cloudflare Worker that sits between all client applications and the Laravel backend API. It provides request routing, KV-based response caching, D1-based stock management, and the Item Database API.

Repository: upvendo-backend-proxyRuntime: Cloudflare Workers (V8 isolates) Entry point: src/index.js


High-Level Architecture

Client Apps (Backoffice, Kiosk, Online Ordering)
         |
         v
  Cloudflare Workers Proxy
    |         |          |            |
    v         v          v            v
 /api/*   /d1-api/*  /item-db-api/* /health
    |         |          |
    v         v          v
 Laravel   D1 Stock   D1 Item
 Backend   Database   Databases

All traffic from frontends goes through the proxy. The proxy decides how to handle each request based on the URL path prefix.


Request Routing

The main fetch() handler in src/index.js routes requests by path:

Path PrefixHandlerDestination
OPTIONS (any path)handleCors()CORS preflight response
/item-database-api/*handleItemDatabaseAPI()D1 item databases (country-based)
/d1-api/*handleStockAPI()D1 stock database
/api/*proxyToLaravel()Laravel backend with KV caching
/health or /health/*handleHealthCheck()Health check responses
Everything else404Not Found

Proxying to Laravel Backend

For /api/* requests, the proxy forwards the request to the Laravel backend and optionally caches the response.

Flow

  1. Check cache -- For GET requests to cacheable endpoints, check KV cache first.
  2. Forward request -- Build a new request to the Laravel backend URL, forwarding all headers (except host, cf-ray).
  3. Add client IP -- Sets X-Upvendo-Client-IP from cf-connecting-ip for accurate IP detection in Laravel.
  4. Handle redirects -- Redirect responses (3xx) are forwarded with CORS headers.
  5. Handle file downloads -- Binary responses (ZIP, Excel, images) are streamed directly without loading into memory.
  6. Cache response -- Successful GET responses to cacheable endpoints are stored in KV with TTL.
  7. Return response -- Add CORS headers and X-Cache: HIT|MISS indicator.

Cache Configuration

Cache key format: api:{userHash}:{apiKeyHash}:{pathname}:{search}

The cache key incorporates the Authorization header and API key to ensure per-user cache isolation.

Cacheable endpoints:

Endpoint PatternCache TTL
/api/settings/*1 hour
/api/constants/*1 hour
/api/menus/*10 minutes
/api/items/*10 minutes
/api/languages/*5 minutes
/api/categories/*5 minutes

Only GET requests are cached. POST, PUT, DELETE requests always go through to the backend.


D1 Stock Management System

The stock management system uses Cloudflare D1 (SQLite at the edge) for low-latency stock tracking. This is critical for kiosk and online ordering to check item availability without hitting the Laravel backend.

Stock Service (src/stock-service.js)

The StockService class provides all stock operations. It uses merchant-specific tables with a naming convention of stocks_merchant_{slug} and stock_reservations_merchant_{slug}.

Table Structure

For each merchant, three tables exist:

stocks_merchant_{slug}

ColumnTypeDescription
item_idTEXTItem identifier
location_idTEXTLocation identifier
stockINTEGERCurrent stock quantity
expired_atINTEGERUnix timestamp when stock expires
updated_atINTEGERLast update timestamp

stock_reservations_merchant_{slug}

ColumnTypeDescription
idTEXTReservation identifier
item_idTEXTItem identifier
reserved_quantityINTEGERReserved quantity
session_idTEXTCart/order session ID
expires_atINTEGERUnix timestamp when reservation expires
created_atINTEGERCreation timestamp

constants_merchant_{slug}

ColumnTypeDescription
keyTEXTConstant key
valueTEXTJSON-encoded value
updated_atINTEGERLast update timestamp

Stock Operations

MethodDescription
getStocks(slug, locationId)Get all stocks for a merchant, with reserved quantities calculated
updateStock(slug, itemId, locationId, stock, expiredAt)Create or replace stock for an item
deleteStock(slug, itemId)Remove stock record for an item
bulkUpdateStocks(slug, updates, alsoTruncate)Batch stock update, optionally clearing existing data
getAvailableStock(slug, itemId)Get available stock (total minus active reservations)

Reservation System

The reservation system prevents overselling by temporarily holding stock during the ordering process.

MethodDescription
createReservation(slug, itemId, qty, sessionId, expirationSec, reservationId)Reserve stock for a cart session
releaseReservationsBySession(slug, sessionId)Release all reservations for a session
cleanupExpiredReservations(slug)Delete expired reservations

Reservation flow:

  1. Customer adds item to cart -> createReservation() with 10-minute default expiration
  2. Available stock = total stock - sum of active (non-expired) reservations
  3. If reservation exists for the session, it is updated rather than duplicated
  4. When customer places order -> reservations are released via releaseReservationsBySession()
  5. If customer abandons cart -> reservations expire automatically

Insufficient stock handling: If available_stock < requested_quantity, the reservation returns { success: false, error: 'Insufficient stock' }.

No stock record: If an item has no stock record in D1, it returns available_stock: 99999 (unlimited).

Constants Operations

MethodDescription
getConstants(slug, key?)Get all or specific constants
setConstant(slug, key, value)Set a single constant
bulkSetConstants(slug, constants)Batch set constants

Analytics

The stock service records analytics for each D1 API call:

sql
INSERT INTO analytics (merchant_slug, endpoint, response_time, timestamp)

Analytics recording is done asynchronously via ctx.waitUntil() to avoid blocking the response.

Health Checks

EndpointResponse
/healthOverall health + analytics count
/health/{merchantSlug}Merchant-specific stock and reservation counts

Item Database API

The Item Database API provides a centralized product database for item information, nutritional data, allergens, and ingredients. It uses a country-based architecture with separate D1 databases per country.

Handler: src/item-database-handler.js

Database Architecture

D1 Databases:
  ITEMS_DB_GLOBAL    -- Global reference data (allergens, ingredients, dietary info)
  ITEMS_DB_BE        -- Belgian items
  ITEMS_DB_FR        -- French items
  ITEMS_DB_NL        -- Dutch items
  ITEMS_DB_DE        -- German items
  ITEMS_DB_ES        -- Spanish items
  ITEMS_DB_PT        -- Portuguese items
  ITEMS_DB_US        -- American items

Each country database contains items scoped by business type. The scope format is {business_type}-{country_code} (e.g., frituur-be, restaurant-fr).

Endpoints

MethodPathAuth RequiredDescription
GET/item-database-api/items?scope={scope}YesList items with pagination and search
GET/item-database-api/items/{id}?scope={scope}YesGet single item by ID
POST/item-database-api/itemsYesCreate item (supports multipart/form-data)
PUT/item-database-api/items/{id}?scope={scope}YesUpdate item
DELETE/item-database-api/items/{id}?scope={scope}YesDelete item
GET/item-database-api/suggestions?scope={scope}&search={term}YesGet single most relevant item for autofill
GET/item-database-api/scopesYesList all scopes with pagination
POST/item-database-api/scopesYesCreate new scope
PUT/item-database-api/scopes/{id}YesUpdate scope name
DELETE/item-database-api/scopes/{id}YesDelete scope
GET/item-database-api/ingredientsYesList ingredients with pagination
POST/item-database-api/ingredientsYesCreate ingredient
PUT/item-database-api/ingredients/{id}YesUpdate ingredient
DELETE/item-database-api/ingredients/{id}YesDelete ingredient
GET/item-database-api/allergensYesList allergens
GET/item-database-api/dietary-preferencesYesList dietary preferences
GET/item-database-api/dietary-supplementsYesList dietary supplements
GET/item-database-api/ingredient-categoriesYesList ingredient categories
GET/item-database-api/item-categoriesYesList item categories
POST/item-database-api/migrations/{scope}YesCreate tables for a scope
GET/item-database-api/constantsYesGet all business types, countries, languages
GET/item-database-api/reference-data/allNo (public)Get all reference data from KV cache

Authentication

Most endpoints require a valid bearer token with has_global_access permission. The reference-data endpoint is public.

Scope Validation

Scope values are validated against:

  • Business types: Predefined list of valid types (frituur, restaurant, etc.)
  • Country codes: BE, FR, NL, DE, ES, PT, US

Invalid scopes return a 400 error with the list of valid values.

Uninitialized Scope Handling

If a scope's tables have not been created yet (SQL "no such table" error), the API returns:

  • Empty results for list endpoints (not a 500 error)
  • A helpful error message with a hint to use the /migrations endpoint

Environment Configuration

Configured in wrangler.toml with per-environment settings.

Environments

EnvironmentWorker NameBackend URL
productionupvendo-api-proxyhttps://backend.upvendo.com
stagingupvendo-api-proxy-staginghttps://staging.backend.upvendo.com
testingupvendo-api-proxy-testinghttps://testing.backend.upvendo.com
devupvendo-api-proxy-devhttp://127.0.0.1:8000

Bindings Per Environment

Each environment has:

  • KV_CACHE -- KV namespace for response caching and reference data
  • STOCK_DB -- D1 database for merchant stock management
  • ITEMS_DB_GLOBAL -- D1 database for global item reference data
  • ITEMS_DB_{CC} -- Country-specific D1 databases (US, BE, FR, NL, DE, PT, ES)

Environment Variables

VariableDescription
ENVIRONMENTCurrent environment name
LARAVEL_BACKEND_URLBackend API base URL
ALLOWED_ORIGINSComma-separated list of allowed CORS origins
D1_API_SECRETSecret for backend-to-proxy D1 API calls
BACKEND_IPSAllowed backend server IPs
CLOUDFLARE_ACCOUNT_IDCloudflare account identifier
CLOUDFLARE_ACCOUNT_HASHCloudflare account hash for image URLs

Allowed Origins (Production)

https://kiosk.upvendo.com
https://backoffice.upvendo.com
https://zestidoo.com
https://zestidoo.be
https://zestidoo.co.uk
https://zestidoo.de
https://zestidoo.fr
https://zestidoo.nl
https://localhost

CORS Handling

CORS is handled centrally via src/cors-helpers.js. Every response includes:

  • Access-Control-Allow-Origin -- Set to the matching allowed origin (not *)
  • Access-Control-Allow-Credentials: true

For file downloads, Content-Disposition is added to Access-Control-Expose-Headers.


Key Patterns for Debugging

  1. Stock discrepancies: Check if the D1 stock data is out of sync with the Laravel backend inventory. The D1 data is populated by the backend and may lag behind.

  2. Cache staleness: KV cache TTLs range from 5 minutes to 1 hour. If a merchant reports stale data, check the X-Cache response header and the relevant endpoint TTL.

  3. Scope initialization: New merchants or business types need their scope tables created via the /migrations endpoint before the item database works.

  4. Reservation leaks: If reservations are not released (e.g., payment flow crashes), expired reservations are cleaned up by cleanupExpiredReservations(). The default reservation expiration is 10 minutes.

  5. Environment mismatch: Ensure the frontend is pointing to the correct proxy environment. Staging frontends should hit upvendo-api-proxy-staging, not production.