Architecture

Commerce ships as three deploy targets in one repo (services/).

Components

services/
├── commerce-api/         FastAPI :8010    Python   Hlavní backend
├── email-renderer/       Fastify :8011   Node 20   MJML compile
└── outbox-dispatcher/    background       Python   LISTEN/NOTIFY worker

commerce-api

The single source of truth. Owns the database, exposes ~129 HTTP endpoints across two prefixes (/admin/2026-01/... and /storefront/2026-01/...), publishes events, enqueues outbox tasks.

Stack: FastAPI 0.115 + SQLAlchemy 2.0 async + asyncpg + Pydantic 2.10

  • Python 3.13. Database is Supabase Postgres 17.6 with row-level security per account_id. Workspace-managed via uv.

email-renderer

Stateless Node service. Compiles MJML templates with Handlebars ctx interpolation, returns final HTML + plain-text fallback + subject. Has its own Dockerfile and unit-tested in tests/render.test.js.

The commerce-api calls it over internal HTTP (default http://localhost:8011/render). No DB access.

outbox-dispatcher

Long-running Python worker. Polls outbox_dispatches with SELECT FOR UPDATE SKIP LOCKED for safe parallelism, listens on the outbox_new channel for instant wakeups. Each task is dispatched to a registered handler — currently task / send_email is the only one shipped, more reserved for connectors and webhooks.

Workers can scale independently of commerce-api (one API pod, many workers).

Run-time topology

storefront app           dashboard / scripts            Resend
  │ X-Storefront-Key       │ Bearer JWT/PAT               │ webhooks
  ▼                        ▼                              ▼
┌───────────────────────────────────────────────────────────────┐
│  commerce-api (FastAPI :8010)                                 │
│   /admin/2026-01/...        /storefront/2026-01/...           │
│   /webhooks/resend                                            │
│                                                                │
│   ▼                                                            │
│   Supabase Postgres 17.6  (~50 tables, RLS-isolated)           │
└──────────┬─────────────────────────────────────┬───────────────┘
           │ outbox_new NOTIFY                   │
           ▼                                     │
   outbox-dispatcher (Python)                    │
           │                                     │
           ├──▶ email-renderer (Node :8011)      │
           │      MJML + Handlebars              │
           │                                     │
           └──▶ Resend API ──────────────────────┘

Data isolation

Every domain table has account_id BIGINT NOT NULL and a row-level security policy:

USING (public.is_admin_mode() OR account_id = public.current_account_id())
WITH CHECK (public.is_admin_mode() OR account_id = public.current_account_id())

The nevios_app Postgres role can't bypass it. commerce-api sets current_account_id per request via SELECT public.set_account_context(...) before any other queries run. RLS enforces that even a bug in application code can't leak across tenants.

Event log + outbox

Every state change publishes an immutable events row (audit log) and inserts outbox_dispatches rows for downstream consumers (email sender, webhooks, future integrations). Both happen in the same DB transaction as the underlying mutation, so events can never fall out of sync with state.

The dispatcher reads from outbox_dispatches with retry/backoff (30s → 2h cap → status='dead').

See Events & outbox for the schema.

What runs where

ConcernWhere
All HTTP routescommerce-api
All DB writescommerce-api
Pricing engine (CZ VAT locked)commerce_api/services/pricing.py
Email renderingemail-renderer (Node)
Email sendingoutbox-dispatcher → Resend
Webhook ingest (Resend events)commerce-api /webhooks/resend
Inventory reservations TTLcommerce-api on checkout create + DB cleanup cron

Next: Authentication.