Young's Mind
02 Architecture

Commerce hosting — design + architecture specs

Young, 2026-10-13: "Package 3 products into single package as sell services."

Commerce hosting — design + architecture specs

Young, 2026-10-13: "Package 3 products into single package as sell services."

This document is the engineering design and architecture spec for the per-tenant hosted commerce product. It covers the tenant template, secrets model, EVE agent template, POS integration, multi-env topology, and the update/rollback pipeline.

Inputs: Tam's four verified studies (live-probed 2026-10-13), Min's deep dive, Mira's business plan. This spec does not re-derive their measurements — it builds on them. Every architectural decision traces to a measured fact in those documents.

Status: DESIGN. No build exists. This is the spec Ace builds from.


1. The product in one paragraph

One sellable package per tenant: Medusa v2 backend + Next.js storefront + Agilo POS + EVE agent, deployed as a per-tenant Docker compose project on Oracle ARM. The customer gets a hosted online shop, in-store POS on their tablets, and an AI staff member that answers customers — one monthly price, we run all of it. The AI-staff layer is the differentiator: neither Medusa Cloud nor Shopify bundles it.

Licence profile: MIT (Medusa + dtc-starter + Agilo POS) + Apache-2.0 (EVE) — clean end-to-end, no competing-use restriction, no per-seat metering.


2. Architecture overview

                      ┌─────────────────────────────────────┐
                      │  Oracle ARM node (aarch64 × 4 OCPU)  │
                      │  23 Gi RAM · 180 G disk free         │
                      └──────────────┬──────────────────────┘
                                     │
              ┌──────────────────────┼──────────────────────┐
              │                      │                      │
   ┌──────────▼──────────┐ ┌─────────▼─────────┐ ┌──────────▼──────────┐
   │  tenant-a (compose)  │ │  tenant-b (compose)│ │  tenant-c (compose)  │
   │  ──────────────────  │ │  ────────────────  │ │  ──────────────────  │
   │  postgres:15-alpine  │ │  postgres:15-alpine│ │  postgres:15-alpine  │
   │  redis:7-alpine      │ │  redis:7-alpine    │ │  redis:7-alpine      │
   │  medusa server       │ │  medusa server     │ │  medusa server       │
   │  medusa worker       │ │  medusa worker     │ │  medusa worker       │
   │  storefront (Next.js)│ │  storefront        │ │  storefront (Next.js)│
   │  eve agent (Nitro)   │ │  eve agent         │ │  eve agent (Nitro)   │
   │  ──────────────────  │ │  ────────────────  │ │  ──────────────────  │
   │  4.8 GB steady       │ │  4.8 GB steady     │ │  4.8 GB steady       │
   └──────────────────────┘ └────────────────────┘ └──────────────────────┘
              │                      │                      │
              └──────────────────────┼──────────────────────┘
                                     │
                      ┌──────────────▼──────────────────────┐
                      │  nginx reverse proxy (per-tenant     │
                      │  domain + TLS termination)          │
                      └─────────────────────────────────────┘
                                     │
                      ┌──────────────▼──────────────────────┐
                      │  Agilo POS (client tablets)         │
                      │  → Admin REST API over HTTPS        │
                      └─────────────────────────────────────┘

Per-tenant isolation is the security model. Each tenant is a separate compose project with its own Postgres volume, Redis container, containers, and secrets file. No 0.0.0.0 binds — all services bind to 127.0.0.1 inside the compose network; only the nginx proxy exposes ports, and only to the tenant's domain.

Capacity: ~4 full tenant stacks (4.8 GB each) or ~6 small tuned stacks (3 GB each) per 23 Gi ARM node. Pilot target: 2–3 tenants on the existing node.


3. Acceptance criteria — design specs

3.1 Tenant template repo

Base: fork medusajs/dtc-starter (MIT, active, the official Medusa v2 starter). The fork is the single template — one repo, version-tagged, all tenants pull from it.

Repo structure:

dtc-starter/                      ← forked template repo
├── Dockerfile                    ← multi-stage prod build (server + worker)
├── docker-compose.prod.yml       ← server/worker split, per-tenant env
├── docker-compose.staging.yml    ← staging variant (same image tag)
├── scripts/
│   ├── onboarding.sh             ← one-command tenant provisioning
│   ├── cutover.sh                ← /health-gated production cutover
│   ├── update.sh                 ← git-tag update pipeline
│   └── rollback.sh               ← prior-tag rollback
├── .env.example                  ← template (no real secrets)
├── .env.secrets.register         ← per-tenant secrets (0600, gitignored)
├── medusa/
│   ├── package.json
│   ├── medusa-config.ts
│   └── src/                      ← custom modules (services catalogue later)
├── storefront/
│   ├── package.json
│   └── src/
└── eve/
    ├── agent/
    │   ├── instructions.md
    │   ├── tools/
    │   ├── connections/
    │   ├── skills/
    │   └── workflows/
    ├── package.json
    └── eve.config.ts

Dockerfile (multi-stage, prod):

# ---- Stage 1: build ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm medusa build

# ---- Stage 2: server ----
FROM node:20-alpine AS server
WORKDIR /app
COPY --from=builder /app/.medusa ./.medusa
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
ENV MEDUSA_WORKER_MODE=server
ENV NODE_ENV=production
EXPOSE 9000
CMD ["sh", "-c", "cd .medusa/server && npm install --omit=dev && npm run predeploy && npm run start"]

# ---- Stage 3: worker ----
FROM node:20-alpine AS worker
WORKDIR /app
COPY --from=builder /app/.medusa ./.medusa
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
ENV MEDUSA_WORKER_MODE=worker
ENV NODE_ENV=production
EXPOSE 9000
CMD ["sh", "-c", "cd .medusa/server && npm install --omit=dev && npm run start"]

docker-compose.prod.yml:

version: "3.9"

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks: [tenant-net]

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks: [tenant-net]

  medusa-server:
    build:
      context: .
      target: server
    env_file: .env.secrets.register
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      MEDUSA_WORKER_MODE: server
    depends_on:
      postgres: { condition: service_healthy }
      redis: { condition: service_healthy }
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:9000/health"]
      interval: 10s
      timeout: 5s
      retries: 12
    networks: [tenant-net]

  medusa-worker:
    build:
      context: .
      target: worker
    env_file: .env.secrets.register
    environment:
      DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      MEDUSA_WORKER_MODE: worker
    depends_on:
      postgres: { condition: service_healthy }
      redis: { condition: service_healthy }
    networks: [tenant-net]

  storefront:
    image: node:20-alpine
    working_dir: /app
    command: sh -c "npm install --omit=dev && npm run start"
    environment:
      MEDUSA_BACKEND_URL: http://medusa-server:9000
      NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY: ${MEDUSA_PUBLISHABLE_KEY}
    depends_on:
      medusa-server: { condition: service_healthy }
    networks: [tenant-net]

  eve-agent:
    image: node:20-alpine
    working_dir: /app
    command: sh -c "npm install --omit=dev && npx eve start"
    env_file: .env.secrets.register
    environment:
      MEDUSA_ADMIN_URL: http://medusa-server:9000
      MEDUSA_ADMIN_TOKEN: ${MEDUSA_ADMIN_TOKEN}
    depends_on:
      medusa-server: { condition: service_healthy }
    networks: [tenant-net]

volumes:
  postgres_data:
  redis_data:

networks:
  tenant-net:
    internal: true

Key design decisions:

  • Server/worker split: MEDUSA_WORKER_MODE=server on the server container, worker on the worker. Admin UI disabled on the worker (DISABLE_MEDUSA_ADMIN=true). Shared Postgres + Redis. This is the production topology from Medusa's docs.
  • Internal network: tenant-net is internal: true — no container exposes a host port. Only nginx (outside the compose project) reaches the services.
  • Health-gated startup: every service has a healthcheck; depends_on uses condition: service_healthy so the stack comes up in order.
  • Shared image, per-tenant env: the Dockerfile is built once per template release. Tenants differ only in their .env.secrets.register and compose project name. This is the "build once, deploy many" model from Tam's study.

Onboarding script (scripts/onboarding.sh):

#!/usr/bin/env bash
# One-command tenant provisioning.
# Usage: ./scripts/onboarding.sh <tenant-name> <domain> [staging|production]
set -euo pipefail

TENANT_NAME="${1:?tenant name required}"
DOMAIN="${2:?domain required}"
ENV="${3:-production}"
COMPOSE_PROJECT="dtc-${TENANT_NAME}-${ENV}"

echo "=== Onboarding tenant: ${TENANT_NAME} (${ENV}) ==="

# 1. Clone template (shallow, latest tag)
git clone --depth 1 --branch "${TEMPLATE_TAG:-main}" \
  "${TEMPLATE_REPO}" "/srv/dtc/${TENANT_NAME}-${ENV}"
cd "/srv/dtc/${TENANT_NAME}-${ENV}"

# 2. Generate secrets register (0600, never in compose)
SECRETS_FILE=".env.secrets.register"
if [ -f "$SECRETS_FILE" ](--f-"$secrets_file"-); then
  echo "ERROR: secrets file already exists. Refusing to overwrite."
  exit 1
fi
{
  echo "# Auto-generated by onboarding.sh — DO NOT COMMIT"
  echo "# $(date -u +%Y-%m-%dT%H:%M:%SZ)"
  echo "POSTGRES_USER=medusa_${TENANT_NAME}"
  echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)"
  echo "POSTGRES_DB=medusa_${TENANT_NAME}"
  echo "REDIS_PASSWORD=$(openssl rand -hex 32)"
  echo "JWT_SECRET=$(openssl rand -hex 32)"
  echo "COOKIE_SECRET=$(openssl rand -hex 32)"
  echo "MEDUSA_PUBLISHABLE_KEY=$(openssl rand -hex 16)"
  echo "MEDUSA_ADMIN_TOKEN=$(openssl rand -hex 32)"
  echo "EVE_API_KEY=$(openssl rand -hex 32)"
} > "$SECRETS_FILE"
chmod 600 "$SECRETS_FILE"

# 3. Create compose project
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml up -d

# 4. Wait for health
echo "Waiting for Medusa /health..."
for i in $(seq 1 30); do
  if docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml \
     exec -T medusa-server wget -q -O /dev/null http://127.0.0.1:9000/health; then
    echo "Medusa healthy."
    break
  fi
  sleep 2
done

# 5. Create admin user
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml \
  exec -T medusa-server npx medusa user \
    -e "admin@${DOMAIN}" \
    -p "$(openssl rand -hex 12)"

# 6. Configure nginx (per-tenant domain + TLS)
#    (delegated to proxy config — see §3.5)

echo "=== Tenant ${TENANT_NAME} provisioned ==="
echo "  Domain: ${DOMAIN}"
echo "  Compose project: ${COMPOSE_PROJECT}"
echo "  Secrets: ${SECRETS_FILE} (0600)"
echo "  Admin: admin@${DOMAIN} (password in secrets file)"

Cutover script (scripts/cutover.sh):

#!/usr/bin/env bash
# /health-gated production cutover.
# Usage: ./scripts/cutover.sh <tenant-name>
# Swaps nginx from staging to production after both health checks pass.
set -euo pipefail

TENANT_NAME="${1:?tenant name required}"
STAGING_PROJECT="dtc-${TENANT_NAME}-staging"
PROD_PROJECT="dtc-${TENANT_NAME}-production"

echo "=== Cutover: ${TENANT_NAME} ==="

# 1. Verify staging health
echo "Checking staging health..."
docker compose -p "$STAGING_PROJECT" -f docker-compose.prod.yml \
  exec -T medusa-server wget -q -O /dev/null http://127.0.0.1:9000/health
docker compose -p "$STAGING_PROJECT" -f docker-compose.prod.yml \
  exec -T eve-agent wget -q -O /dev/null http://127.0.0.1:3000/eve/v1/health
echo "Staging healthy."

# 2. Verify production health
echo "Checking production health..."
docker compose -p "$PROD_PROJECT" -f docker-compose.prod.yml \
  exec -T medusa-server wget -q -O /dev/null http://127.0.0.1:9000/health
docker compose -p "$PROD_PROJECT" -f docker-compose.prod.yml \
  exec -T eve-agent wget -q -O /dev/null http://127.0.0.1:3000/eve/v1/health
echo "Production healthy."

# 3. Swap nginx upstream
#    (delegated to proxy config — see §3.5)

echo "=== Cutover complete: ${TENANT_NAME} ==="

3.2 Per-tenant secrets register

Model: one .env.secrets.register file per tenant, per environment. Generated by onboarding.sh. Never committed to git. Never embedded in compose files.

Rules:

RuleRationale
chmod 600 on the secrets fileonly the tenant's containers read it
env_file: .env.secrets.register in composeDocker reads it directly; no shell interpolation
openssl rand -hex 32 for all secrets256-bit entropy, no external dependency
.env.secrets.register in .gitignorenever committed, never pushed
.env.example in gittemplate with dummy values, safe to commit
Secrets never in compose YAMLcompose files are version-controlled; secrets are not
Secrets never in environment variables passed to docker composeshell history, process table exposure

Secrets inventory:

SecretPurposeGenerated
POSTGRES_USERPostgres usernamemedusa_<tenant>
POSTGRES_PASSWORDPostgres passwordopenssl rand -hex 32
POSTGRES_DBPostgres database namemedusa_<tenant>
REDIS_PASSWORDRedis AUTH passwordopenssl rand -hex 32
JWT_SECRETMedusa JWT signingopenssl rand -hex 32
COOKIE_SECRETMedusa cookie signingopenssl rand -hex 32
MEDUSA_PUBLISHABLE_KEYStorefront publishable key (read-scope)openssl rand -hex 16
MEDUSA_ADMIN_TOKENAdmin API token for EVE agent HIL workflowsopenssl rand -hex 32
EVE_API_KEYEVE agent API keyopenssl rand -hex 32

Key scoping:

  • Storefront agent tools use MEDUSA_PUBLISHABLE_KEY (read-scope) — can read products, carts, orders but cannot mutate.
  • EVE agent HIL workflows use MEDUSA_ADMIN_TOKEN (write-scope) — only invoked after human approval through the HIL gate.
  • Admin UI uses the admin user credentials created during onboarding.

3.3 EVE agent template

Base: eve init scaffold (Apache-2.0, beta, filesystem-first). Self-hosted as a Nitro server beside the tenant's Medusa stack.

Agent structure:

eve/
├── agent/
│   ├── instructions.md          ← sales/support persona, tenant-tuned
│   ├── tools/
│   │   ├── products.ts           ← GET /store/products (read)
│   │   ├── carts.ts              ← GET /store/carts (read)
│   │   ├── checkout.ts           ← POST /store/carts/:id/complete (write, HIL-gated)
│   │   └── order-status.ts       ← GET /store/orders/:id (read)
│   ├── connections/
│   │   └── medusa.ts            ← Medusa Admin API connection (zod-validated config)
│   ├── skills/
│   │   ├── refund-playbook.md    ← refund policy + approval workflow
│   │   └── checkout-flow.md      ← checkout assistance + common issues
│   └── workflows/
│       ├── order-status-watcher.ts  ← durable: poll order status, notify customer
│       └── hil-refund-gate.ts       ← HIL: pause → admin approval → execute
├── package.json
└── eve.config.ts

connections/medusa.ts:

import { defineDynamic } from "@eve/sdk/connections";
import { z } from "zod";

export default defineDynamic({
  name: "medusa",
  configSchema: z.object({
    MEDUSA_ADMIN_URL: z.string().url(),
    MEDUSA_ADMIN_TOKEN: z.string().min(1),
  }),
  // Secrets come from environment (0600 fleet hygiene carries).
  // Never stored in source, never in compose files.
});

tools/products.ts (read-scope):

import { defineTool } from "@eve/sdk/tools";

export default defineTool({
  name: "list_products",
  description: "List available products with pricing and inventory",
  parameters: z.object({
    limit: z.number().optional().default(20),
    offset: z.number().optional().default(0),
  }),
  // Uses MEDUSA_PUBLISHABLE_KEY (read-scope)
  async execute({ limit, offset }, { connections }) {
    const medusa = await connections.medusa();
    return medusa.store.products.list({ limit, offset });
  },
});

workflows/hil-refund-gate.ts (HIL skeleton):

import { defineWorkflow } from "@eve/sdk/workflows";

export default defineWorkflow({
  name: "hil-refund-gate",
  description: "Process refund with human-in-the-loop approval",
  // HIL: pause → admin approval → execute against Admin API
  // Uses MEDUSA_ADMIN_TOKEN (write-scope, HIL-gated)
  async execute({ orderId, amount, reason }, { connections, human }) {
    // 1. Pause and request human approval
    const approval = await human.approve({
      action: "refund",
      orderId,
      amount,
      reason,
    });
    if (!approval.granted) {
      return { status: "rejected", reason: approval.reason };
    }
    // 2. Execute refund via Admin API
    const medusa = await connections.medusa();
    return medusa.admin.refunds.create({ orderId, amount, reason });
  },
});

Version pinning:

{
  "dependencies": {
    "@eve/sdk": "5.0.0-beta.2",
    "@workflow/core": "5.0.0-beta.2",
    "@workflow/runtime": "5.0.0-beta.2"
  }
}

EVE beta risk mitigation: pin to exact versions (not ranges). The update pipeline includes a per-tenant EVE health check (/eve/v1/health) before the tenant is marked healthy. If EVE breaks between pinned versions, the tenant stays on the last known-good version until a fix is validated in staging.


3.4 POS integration

Component: Agilo POS (Agilo/medusa-pos-starter, MIT, Expo React Native).

Architecture: Agilo POS is a client-side app running on the tenant's tablets. It connects to the tenant's Medusa Admin REST API over HTTPS. No backend changes required — the POS is a consumer of the same Admin API that the EVE agent uses.

Agilo POS (tablet)
    │
    │  HTTPS → Admin REST API
    │  (admin credentials from POS setup wizard)
    │
    ▼
Medusa server (tenant compose project)
    │
    ├── /admin/auth/login
    ├── /admin/orders (draft orders)
    ├── /admin/products
    └── /admin/customers

Setup flow:

  1. Tenant installs Agilo POS on their tablets (APK or Expo Go).
  2. POS setup wizard: enter tenant's Medusa Admin URL + admin credentials.
  3. POS fetches regions, sales channels, stock locations from Admin API.
  4. Staff log in with admin credentials (per-staff credentials are a future add — the current model uses one admin credential per tenant, which is coarse but functional for pilot).
  5. POS creates draft orders → sync to Admin → appear in Medusa admin alongside online orders.

Manual payment path (documented):

Agilo POS has no integrated payment processing (confirmed in Tam's study, MVP gap). The manual payment plugin path exists in Medusa:

  1. POS creates a draft order with items.
  2. At checkout, staff select "Manual Payment" as the payment method.
  3. Staff collect payment externally (cash, bank transfer, terminal).
  4. Staff mark the order as paid in Medusa admin.
  5. Order moves from draft to confirmed.

This is the pilot payment path. Terminal integrations (Stripe/Adyen) are on Agilo's roadmap but not yet available. The pilot sells "website + AI staff" with POS as a Phase 2 add — the AI staff layer is the differentiator, not POS payments.

POS auth model (current):

AspectCurrentFuture
CredentialsOne admin credential per tenantPer-staff roles
AuthAdmin REST API tokenStaff-scoped tokens
PaymentManual (cash/invoice)Terminal integration
OfflineNo (cached browse only)Full offline mode

3.5 Multi-env topology

Model: staging + production per client = two compose projects, same image tag, separate DBs/Redis.

tenant-a-staging/          ← compose project: dtc-tenant-a-staging
├── postgres (staging DB)
├── redis
├── medusa server
├── medusa worker
├── storefront
└── eve agent

tenant-a-production/       ← compose project: dtc-tenant-a-production
├── postgres (production DB)
├── redis
├── medusa server
├── medusa worker
├── storefront
└── eve agent

Same image tag: both environments pull the same Docker image tag from the template repo. The only difference is the .env.secrets.register (different secrets per environment) and the compose project name.

nginx routing:

# staging
server {
    listen 443 ssl;
    server_name staging.<tenant-domain>.com;
    location / {
        proxy_pass http://127.0.0.1:8001;  # staging storefront
    }
    location /eve/ {
        proxy_pass http://127.0.0.1:3001/;  # staging eve agent
    }
    location /.well-known/workflow/ {
        proxy_pass http://127.0.0.1:3001;  # staging eve workflows
    }
}

# production
server {
    listen 443 ssl;
    server_name <tenant-domain>.com;
    location / {
        proxy_pass http://127.0.0.1:8000;  # production storefront
    }
    location /eve/ {
        proxy_pass http://127.0.0.1:3000/;  # production eve agent
    }
    location /.well-known/workflow/ {
        proxy_pass http://127.0.0.1:3000;  # production eve workflows
    }
}

Cutover: cutover.sh verifies both staging and production health, then swaps the nginx upstream from staging to production. Rollback = swap back.


3.6 Update pipeline

Model: git-tagged template → pull → predeploy (auto-migrate) → restart → health re-check. Rollback = prior tag.

Update script (scripts/update.sh):

#!/usr/bin/env bash
# Git-tag update pipeline.
# Usage: ./scripts/update.sh <tenant-name> <env> <new-tag>
set -euo pipefail

TENANT_NAME="${1:?tenant name required}"
ENV="${2:?environment required}"
NEW_TAG="${3:?new tag required}"
COMPOSE_PROJECT="dtc-${TENANT_NAME}-${ENV}"
DEPLOY_DIR="/srv/dtc/${TENANT_NAME}-${ENV}"

echo "=== Update: ${TENANT_NAME} (${ENV}) → tag ${NEW_TAG} ==="

cd "$DEPLOY_DIR"

# 1. Record current tag for rollback
CURRENT_TAG=$(git rev-parse --short HEAD)
echo "Current tag: ${CURRENT_TAG}"

# 2. Pull new tag
git fetch --tags --depth 1 origin "$NEW_TAG"
git checkout "$NEW_TAG"

# 3. Rebuild image (shared image, new tag)
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml build

# 4. Predeploy (auto-migrate)
echo "Running predeploy (auto-migrate)..."
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml \
  run --rm medusa-server npm run predeploy

# 5. Restart services
echo "Restarting services..."
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml up -d

# 6. Health re-check
echo "Waiting for health..."
for i in $(seq 1 30); do
  if docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml \
     exec -T medusa-server wget -q -O /dev/null http://127.0.0.1:9000/health; then
    echo "Medusa healthy."
    break
  fi
  sleep 2
done

# 7. EVE health check
for i in $(seq 1 30); do
  if docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml \
     exec -T eve-agent wget -q -O /dev/null http://127.0.0.1:3000/eve/v1/health; then
    echo "EVE agent healthy."
    break
  fi
  sleep 2
done

echo "=== Update complete: ${TENANT_NAME} (${ENV}) → ${NEW_TAG} ==="
echo "  Rollback: git checkout ${CURRENT_TAG} && docker compose up -d"

Rollback script (scripts/rollback.sh):

#!/usr/bin/env bash
# Prior-tag rollback.
# Usage: ./scripts/rollback.sh <tenant-name> <env>
set -euo pipefail

TENANT_NAME="${1:?tenant name required}"
ENV="${2:?environment required}"
COMPOSE_PROJECT="dtc-${TENANT_NAME}-${ENV}"
DEPLOY_DIR="/srv/dtc/${TENANT_NAME}-${ENV}"

echo "=== Rollback: ${TENANT_NAME} (${ENV}) ==="

cd "$DEPLOY_DIR"

# 1. Checkout prior tag
PRIOR_TAG=$(git rev-parse --short HEAD~1)
echo "Rolling back to: ${PRIOR_TAG}"
git checkout "$PRIOR_TAG"

# 2. Rebuild image
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml build

# 3. Restart (no predeploy — schema is already migrated forward;
#    rollback is a code rollback, not a schema rollback)
docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml up -d

# 4. Health re-check
echo "Waiting for health..."
for i in $(seq 1 30); do
  if docker compose -p "$COMPOSE_PROJECT" -f docker-compose.prod.yml \
     exec -T medusa-server wget -q -O /dev/null http://127.0.0.1:9000/health; then
    echo "Medusa healthy."
    break
  fi
  sleep 2
done

echo "=== Rollback complete: ${TENANT_NAME} (${ENV}) → ${PRIOR_TAG} ==="

Pipeline summary:

git tag v1.2.3 → push to template repo
    │
    ▼
update.sh <tenant> <env> v1.2.3
    │
    ├── git fetch + checkout v1.2.3
    ├── docker compose build (new image)
    ├── predeploy (auto-migrate)
    ├── docker compose up -d (restart)
    ├── health check: /health (Medusa)
    └── health check: /eve/v1/health (EVE)
         │
         ▼
    tenant live on v1.2.3

rollback.sh <tenant> <env>
    │
    ├── git checkout v1.2.2 (prior tag)
    ├── docker compose build
    ├── docker compose up -d
    └── health check: /health
         │
         ▼
    tenant live on v1.2.2

Key design decisions:

  • Checkout-before-cutover: the old image stays running until the new one is built and healthy. Zero-downtime deploys.
  • Predeploy auto-migrate: npm run predeploy runs medusa db:migrate on every update. This is the Medusa-documented update hook.
  • Health-gated: the tenant is not marked healthy until both Medusa /health and EVE /eve/v1/health return OK.
  • Rollback = prior tag: no schema rollback (migrations are forward-only). The prior tag runs against the current schema. If a migration is incompatible, the fix is a new migration + new tag, not a rollback.

Tam's verified studies (inputs, not re-derived)

StudyWhat it establishes
medusa-hosting-docker-study.md4-service compose stack, server/worker split, per-tenant isolation, Oracle ARM capacity (~4 stacks at 4.8 GB)
medusa-nextjs-commerce-study.mdMedusa v2 MIT, dtc-starter, Agilo POS (MIT, Admin REST, no payments), licence matrix
medusa-eve-integration-study.mdEVE agent architecture, useEveAgent(), HIL gates, key scoping, build sequencing
hermes-eve-cohosting-study.mdEVE framework vs Hermes, co-hosting RAM table, self-host topology, version pinning

Min's deep dive

DocumentWhat it contributes
3-package-deep-dive.mdProduct restatement, gaps (services vs goods, EVE beta risk, POS payments gap, multi-tenant memory, Oracle free tier), architecture decision table

Mira's business plan

DocumentWhat it contributes
business-plan-hosted-commerce-pos-ai-staff.mdPricing tiers ($79/$149/+$49), unit economics, competitor bench, GTM sequencing, risk register

Tam's unified package deep-dive

DocumentWhat it contributes
unified-package-deepdive.mdPackaging mechanics (feature toggles, single deployment unit, tier ladder, update/rollback, data/backup), commercial inputs

5. Open questions for Young

These are decisions that change the build. Each has a recommendation.

Q1: Services vs physical goods for the pilot

Question: Is the target customer selling physical goods, services, or both?

Recommendation: pilot with physical/simple goods. The services catalogue module (custom Medusa module for bookable services) is scoped after the first tenant ships. This matches Tam's study recommendation and Min's deep dive.

Why: a services business needs booking/scheduling, not a shopping cart. These are different products wearing the same brand. Pick one vertical for the pilot.

Q2: POS payments at launch or Phase 2?

Question: Does the pilot need working POS payments from day one?

Recommendation: Phase 2. Sell "website + AI staff" first — the AI staff layer is the differentiator. POS payments can follow when Agilo's terminal roadmap lands.

Why: Agilo POS has no integrated payments (manual path only). For a product sold as "website + POS + AI staff," the POS being unable to take payments is a gap in the core value prop. But the AI staff layer is the wedge — lead with it.

Q3: EVE beta risk acceptance

Question: EVE is beta (Vercel beta terms, API churn expected). What's the rollback story when EVE breaks?

Recommendation: accept with mitigations. Pin exact versions, per-tenant EVE health check in the update pipeline, adopt for tenants first (fleet later).

Why: pinning helps with reproducibility. The update pipeline includes a per-tenant EVE health check before the tenant is marked healthy. If EVE breaks between pinned versions, the tenant stays on the last known-good version.

Q4: Oracle ARM free tier as production foundation?

Question: What happens when the first paying tenant goes down because Oracle reclaimed the ARM capacity?

Recommendation: pilot on Oracle ARM (free, good for validation), but the production target should be a paid VPS or dedicated node. The per-tenant compose model makes migration trivial.

Why: Oracle ARM free tier is generous but it's a free tier — no SLA, no guarantee of availability. For a paid product hosted on a free tier, the COGS is near-zero but the risk is total.

Q5: Multi-tenant memory ownership

Question: Is per-tenant memory stored in the tenant's own stack (EVE file provider, tenant-scoped) or in a shared memory service?

Recommendation: start with EVE's file provider (tenant-scoped, zero infra). Revisit when a second tenant demands memory portability.

Why: the former is simpler but means memory doesn't survive tenant migration. The latter is more complex but enables cross-tenant learning (with consent).


6. Not established

Stated, not smoothed.

  • No build exists. This is a design spec. Ace builds from it.
  • EVE agent tools are specced, not tested. The 4 starter tools (products, carts, checkout, order-status) are designed against Medusa's Store API and EVE's tool conventions, but have not been executed against a live tenant.
  • Agilo POS manual payment path is documented, not tested. The flow (draft order → manual payment → mark paid) is the Medusa-documented path but has not been exercised end-to-end.
  • nginx config is specced, not deployed. The per-tenant domain + TLS routing is designed but not yet configured on the Oracle ARM node.
  • Onboarding script is specced, not run. The script is designed against the template repo structure but has not been executed against a live template.
  • Update/rollback pipeline is specced, not tested. The git-tag → pull → predeploy → restart → health flow is designed but not yet exercised with a real tenant.
  • Services catalogue module is not scoped. The custom Medusa module for bookable services is a future build item, not part of the pilot.

7. Build sequencing (Ace lane, off this spec)

  1. Tenant template repo: fork dtc-starter + prod Dockerfile + docker-compose.prod.yml
    • onboarding script + cutover script.
  2. Per-tenant secrets register: .env.secrets.register generation in onboarding script, 0600, gitignored.
  3. EVE agent template: eve init + medusa.ts connection + 4 starter tools + 2 skills + HIL workflow skeleton + version-pinned @workflow/* line.
  4. One demo tenant: storefront + chat + one skill live, gated on /health + /eve/v1/health both OK.
  5. POS app: Agilo POS pointing at the demo tenant backend.
  6. Update pipeline: git-tagged template → pull → predeploy → restart → health re-check. Rollback = prior tag.
  7. Multi-env: staging + production compose projects for the demo tenant.

8. Design rules, and the defect each one closes

RuleSourceThe defect it closes
Per-tenant compose projectTam's studyshared cluster multi-tenancy is for scaling one tenant, not isolating many
Shared image, per-tenant envTam's studyrebuilding per-tenant would saturate 4 vCPUs
0.0.0.0 binds forbiddentask briefper-tenant isolation is the security model
Secrets 0600, never in composefleet hygienecompose files are version-controlled; secrets are not
Health-gated cutoverMedusa docsno stack is "live" until /health returns OK
Checkout-before-cutoverTam's studyzero-downtime deploys
Predeploy auto-migrateMedusa docsnpm run predeploy runs medusa db:migrate on every update
Version-pinned EVETam's studyEVE beta API churn
Internal compose networkthis specno container exposes a host port; only nginx reaches services
env_file for secretsthis specDocker reads secrets directly; no shell interpolation

9. Sources

  • Tam's four verified studies (live-probed 2026-10-13): medusa-hosting-docker-study.md, medusa-nextjs-commerce-study.md, medusa-eve-integration-study.md, hermes-eve-cohosting-study.md
  • Min's deep dive: 3-package-deep-dive.md
  • Mira's business plan: business-plan-hosted-commerce-pos-ai-staff.md
  • Tam's unified package: unified-package-deepdive.md
  • Medusa docs: docs.medusajs.com/learn/installation/docker, learn/deployment, learn/production/worker-mode
  • EVE docs: eve.dev/docs/guides/frontend/overview, docs/guides/deployment/self-hosting, docs/concepts/project-structure
  • GitHub: medusajs/dtc-starter (MIT), Agilo/medusa-pos-starter (MIT), vercel/eve (Apache-2.0)
Copyright © 2026