# Architecture

## Principle

Keep it "almost no server." Everything that *can* be static/config lives in Shopify
(metaobjects, Liquid, theme JS, Flow). Only the operations that are impossible to do
safely from the browser go through one small serverless function.

## Components

### 1. Metaobjects (config)
`wineclub_cycle` + `wineclub_tier_pool`. Edited in the native Shopify admin
(Settings → Custom data → Metaobjects). No custom admin app to build or host.
See [data-model.md](data-model.md).

### 2. Storefront portal (Liquid + JS)
`theme/sections/wine-club-customize.liquid` renders on a customer-authenticated page.
Liquid resolves the logged-in customer's tier + the active cycle's pool, emits it as
JSON via `snippets/wine-club-customize-data.liquid`, and the JS
(`assets/wine-club-customize.js`) drives the swap/add/upgrade UI and live pricing.
Pure client-side until "Lock in" — then it POSTs to the App Proxy.

### 3. App Proxy Worker (the only backend)
A Cloudflare Worker mounted at `/apps/wineclub/*` via Shopify App Proxy. Three routes:

| Route                       | Does                                                        |
|-----------------------------|------------------------------------------------------------|
| `POST /apps/wineclub/save`  | Validate selection against the tier pool, write customer metafield |
| `POST /apps/wineclub/charge`| Create one-time charge (draft order / Appstle one-time API) for extras |
| `POST /apps/wineclub/upgrade` | Swap the customer's Appstle subscription plan (permanent tier change) |

Every request carries Shopify's App-Proxy HMAC signature; the Worker verifies it before
touching the Admin API. Admin API token lives only in the Worker's secret store — never
in the theme.

### 4. Shopify Flow (automation)
- **Reminder email** — scheduled/Flow trigger fires at `close_at − 10 days`, sends the
  personalized portal link + deadline. See [flows/01-reminder-email.md](../flows/01-reminder-email.md).
- **Fulfillment generation** — at `close_at`, generate tagged orders from saved
  selections. See [flows/02-fulfillment-generation.md](../flows/02-fulfillment-generation.md).
- **Fallback** — members with no saved selection get the admin default fulfilled.
  See [flows/03-fallback.md](../flows/03-fulfillment-generation.md).

## Request flow — customer customizes

```
Customer opens /pages/wine-club-customize
        │
        ▼
Liquid: who is the customer? → tier → active cycle → tier_pool
        │  emits JSON (default box, swap pool, live prices, deadline)
        ▼
JS renders portal (mockup UI) · swap/add/upgrade · live price summary
        │
        ▼  "Lock in my selection"
POST /apps/wineclub/save  ──HMAC──▶  Worker verifies → validates pool
                                          → writes customer.wineclub.selection
        │  (if extras present)
        ▼
POST /apps/wineclub/charge ──────▶  Worker → one-time charge → receipt email
```

## Request flow — cycle close (automated)

```
close_at reached
        │
        ▼  Flow scheduled run
For each active member:
   selection saved?  ── yes ─▶ generate order from selection (swaps + add-ons)
                     ── no  ─▶ generate order from tier default   (fallback)
        │
        ▼
Tag order per tier (packing/shipping flags) · set fulfillmentStatus
```

## Security notes

- Selection writes are server-validated against the tier's `swap_pool` and
  `bottle_count`; a tampered client payload can't inject an off-pool wine or wrong price
  (prices are re-read live in the Worker, never trusted from the client).
- App Proxy HMAC verified on every request.
- No Admin credentials, Appstle keys, or PII in theme/JS.

## Cost / overhead

- Cloudflare Workers free tier covers this volume comfortably.
- No always-on server, no DB (state = metaobjects + customer metafields).
- Config changes are done by staff in Shopify admin, not by developers.
