Wocart

Architecture

Wocart is three applications on Cloudflare Workers, sharing one D1 database:

Code
Storefront (Astro) ──/store/*──▶ API server (Hono) ◀──/api/*── Dashboard (Astro) │ │ carriers ──/webhooks └──▶ Meta Conversions API, your webhooks, Telegram │ D1 (data) · R2 (uploads) · KV (rate limits, AI-agent sign-in) · a queue
ApplicationFolderWhat it doesRuns on
API servercod-serverEvery rule: orders, prices, delivery, carriers, events. The merchant API (/api/*), the storefront API (/store/*), carrier callbacks (/webhooks/*), media (/media/*) and the MCP server (/mcp)Cloudflare Workers, D1, R2, KV, Queues, Workflows
Dashboardcod-client-astroThe merchant's back office. Its pages are static; a small Worker handles sign-inCloudflare Workers
Storefrontcod-astro/theme-00The shop shoppers see, rendered per requestCloudflare Workers or Vercel

A fourth folder, cod-shared, holds the database schema, permissions and error codes the other three import. It is not deployed on its own.

Who talks to whom

  • The storefront reads the store through /store/* with the store's API key, on the server. It never touches the database, and the key never reaches a shopper's browser. Themes are built on the Storefront SDK and the Theme SDK.
  • The dashboard signs a user in and gives the browser a short-lived token; every screen then calls /api/* with it. Permissions are checked by the API server, never by the dashboard.
  • Your own code calls /api/* with a merchant API key (Authentication).
  • AI agents connect to /mcp through OAuth and act with the signed-in user's permissions (MCP server).

An order's life

  1. Placed. The shopper's order reaches POST /store/orders. The server prices every line from the catalog; a browser's price is never trusted. The order is new.

  2. Processed. The merchant confirms it by phone and prepares it: new → confirmed → preparing → ready, or unreachable, or cancelled (PATCH /api/orders/{id}/status).

  3. Handed off. A ready order gets one shipment, to a carrier or to the store's own driver (POST /api/orders/{id}/shipment). A carrier is called once per order; when its answer is lost, the parcel is looked up, never sent twice.

  4. Delivered. The shipment follows the carrier:

    • Yalidine and ZR Express call /webhooks/* on every change;
    • NOEST, Guepex and the EcoTrack couriers are checked every five minutes, and on demand;
    • a driver's progress is reported from the dashboard.

    Delivery, a return and a refusal at the door each happen once, with their effects: stock back, the customer's spend, the driver's cash.

An order's status is its processing only; everything about the delivery lives on its shipment.

Events

Every change to an order or a shipment records a business event (order.created, order.confirmed, shipment.delivered, …) in the same database write as the change itself: the change and its event are saved together or not at all. The server then delivers each event to whoever subscribed, and retries a delivery that fails:

A queue wakes the delivery at once; a five-minute schedule catches anything left. A failed delivery is retried and can be replayed from the dashboard.

Background work

ScheduleWhat it does
Every five minutesDelivers events, processes the carriers' webhook inbox, checks the carriers that send no webhooks
Every hourMarks checkouts left unfinished for more than 30 minutes as abandoned

Photos uploaded with a review or by an AI agent are prepared in the background (Cloudflare Workflows), after the answer.

Data

Everything a store has lives in its own D1 database: catalog, orders, shipments, customers, settings, translations, events. Files are kept on the store's media host, ImageKit or R2 (Media); the database holds only their keys. Nothing is shared between stores.