Skip to content

TCG Platform — Staging Environment

Status: Deployed ✅ Deployed: 2026-04-22 — Phase 1 (Foundation). See Phase 1 Kickoff Plan. URL: https://tcg-staging.exzentcg.com Snapshot: initial-deploy Phase 2 applied: applied 2026-04-22 ✅ — TCG module scaffold, seed fixtures, stock-location scenarios. See Phase 2 Findings. Phase 3 applied: in progress ⏳ — Shopee inbound connector live; awaiting end-to-end validation with real Shopee credentials. See Phase 3 Findings. (Status flips to ✅ after a real test order completes the inbound flow + status update + escrow capture.) Phase 5 applied: applied 2026-05-12 ✅ — Operator dashboard live at https://dashboard-staging.exzentcg.com (Cloudflare Worker, gated by Access). Server-side /admin/dashboard/* endpoints (orders / exceptions / inventory / variant-editor / staff) all rendering real data. See Phase 5 Findings.


Purpose

Hosts the Medusa application for the TCG Commerce Operations Platform in a staging environment on the Proxmox host. Used during Phases 1–9 for integration testing, UAT, and parallel-run validation before Phase 10 go-live. This container runs only the Medusa server + its Postgres + Redis sidecars — no customer-facing traffic.

Why staging on Proxmox? Mirrors the homelab self-hosted pattern already used for n8n (CT 102) and Homepage (CT 103). No cloud spend until production host is decided in Phase 10. Cloudflare Tunnel + Access gates external access the same way as other internal services.

Staging runs the production bundle, not the Vite dev server

Per ExzenTCG/tcg-platform#6, the Docker image is a two-stage build — the builder runs yarn medusa build and the runtime serves the prebuilt admin from .medusa/server/public/admin via medusa start. No medusa develop, no Vite HMR, no port 24678 WebSocket. Staging mirrors the production deployment pattern so that what works here also works when Phase 10 go-live flips to the production host.


Deployment Details

Property Value
CT ID 105
Hostname tcg-staging
IP 192.168.0.55
Medusa port 9000
Postgres port 5432 (container-internal only)
Redis port 6379 (container-internal only)
RAM 4096 MB
Swap 2048 MB
Disk 20 GB (Medusa + Postgres + Redis + node_modules)
DNS 1.1.1.1 (same as CT 103 — router DNS has known lookup issues)
Public URL https://tcg-staging.exzentcg.com
Auth Cloudflare Access (team, 24-hour session)
Snapshot initial-deploy (taken 2026-04-22 after first green admin login)
Phase 2 snapshot phase-2-seeded (taken 2026-04-22 after seed + stock scenarios)
Phase 3 snapshot phase-3-shopee-live (taken after staging validation completes)
Phase 5 PR-A snapshot pre-phase5-pra-test (taken 2026-05-09 before monorepo cutover smoke; safe to drop after staging stable for 1 week)
Phase 5 PR-B snapshot pre-phase5-prb-test (taken 2026-05-09 before staff RBAC + cookie-scope smoke; safe to drop after staging stable for 1 week)
Phase 5 PR D-G snapshot pre-phase5-prd-prg-test (taken 2026-05-10 before first rebuild after PRs D/E/F/G; the rebuild crashlooped on the yarn hoist break — see troubleshooting — and was rolled back to this snapshot; superseded by pre-yarn-hoist-fix, safe to drop on/after 2026-05-19)
Yarn-hoist-fix snapshot pre-yarn-hoist-fix (taken 2026-05-12 before deploying PR #67 which fixed the hoist break; safe to drop on/after 2026-05-19)
Repo layout (post-monorepo, 2026-05-09) Medusa lives at apps/server/ after tcg-platform PR #51 (yarn workspaces). Compose runbook is now cd apps/server && docker compose up. The apps/server/docker-compose.yml has name: tcg-platform locked so volumes/networks remain tcg-platform_* regardless of cwd (see Phase 5 implementation plan PR A.6 corrigendum).
Required .env keys All Phase 3 keys plus COOKIE_DOMAIN=.exzentcg.com (added 2026-05-09 by tcg-platform PR #52) and PUBLIC_API_BASE_URL=https://tcg-staging.exzentcg.com (Phase 7 dynamic Shopee push/OAuth URL derivation). COOKIE_DOMAIN MUST be in apps/server/.env AND in the compose environment: block — Medusa's projectConfig.cookieOptions.domain reads process.env.COOKIE_DOMAIN. Verify with docker compose exec -T medusa sh -c 'echo $COOKIE_DOMAIN $PUBLIC_API_BASE_URL' after deploy.
Cloudflare Access service token (Worker → Medusa) tcg-dashboard-worker (created 2026-05-10 via Zero Trust → Access → Service Auth). Bound to a Service Auth policy on the existing tcg-staging Access app (alongside, not replacing, the operator-SSO policy). Worker side: CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET set as Worker secrets on tcg-dashboard-staging. Medusa side: nothing to configure — Access simply waves through any inbound request bearing the matching CF-Access-Client-Id + CF-Access-Client-Secret headers. See apps/dashboard/lib/medusa.ts::getCfAccessHeaders() for emit logic.
Dashboard static asset Access bypass Required for dashboard-staging.exzentcg.com/_next/static/*, /_next/image*, and /favicon.ico. Without a higher-precedence Bypass / Everyone Access app for these paths, Cloudflare Access returns cross-origin 302s for Next.js client bundles and server-action assets; SSR HTML renders but client islands do not hydrate. Captured after the staging import-picker regression and documented in apps/dashboard/README.md by tcg-platform#130.

Resource sizing rationale

Medusa + Postgres + Redis + node_modules/ (~300MB) + admin-dashboard build artefacts comfortably fit in 4GB RAM / 20GB disk for a single-merchant workload. Bump RAM to 8GB if connector polling jobs (Phase 3+) cause thrashing under load tests.

20 GB rootfs is tight under repeated rebuild cycles

After ~6 image rebuilds during Phase 2 iteration, the rootfs hit 50% full (~9.1 GB used, majority reclaimable BuildKit cache and stale images). Run docker system prune -af after each merge cycle to reclaim space — confirmed to recover ~11 GB cleanly during the Phase 5 #67 rebuild on 2026-05-12. If the container is used heavily during Phase 3+ connector iteration, consider bumping rootfs to 40 GB. That is a separate infra change and does not affect the current deployment.

Don't use --volumes once seed data has landed

The Phase 2 runbook used docker system prune -af --volumes because the Postgres volume was disposable then. Post-Phase 3 the Postgres volume holds real seeded products, real Shopee staging orders, real connector tokens — wiping it costs a re-seed + re-OAuth-bootstrap. Use docker system prune -af (no flag) from this point on. The Postgres volume tcg-platform_postgres-data is reclaimable-looking but is not unused; the pruner does the right thing without the flag.

This is staging, not production

Production host decision is deferred to Phase 10 (Go-live). Do NOT treat this container as production — data resets are acceptable during Phases 1–9 if schema migrations go sideways.


Deployment Plan

Step 1 — Create LXC

From the Proxmox host (192.168.0.200):

pct create 105 local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst \
  --hostname tcg-staging \
  --cores 2 \
  --memory 4096 \
  --swap 2048 \
  --rootfs local-lvm:20 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.0.55/24,gw=192.168.0.1,firewall=1 \
  --nameserver 192.168.0.1 \
  --features nesting=1 \
  --onboot 1 \
  --start 1

Step 2 — Install Docker + set DNS

pct enter 105
apt update && apt upgrade -y && apt install curl -y
curl -fsSL https://get.docker.com | sh
exit

# Fix DNS from Proxmox host (router DNS has known ghcr.io lookup issues — see CT 103)
pct set 105 --nameserver 1.1.1.1
pct reboot 105

Step 3 — Deploy Medusa + sidecars

The Medusa repo is ExzenTCG/tcg-platform (created in Phase 1, task D1). As of 2026-05-09 the repo is a yarn-workspaces monorepo — Medusa lives at apps/server/ (Phase 5 PR #51). On the staging CT:

pct enter 105
mkdir -p /opt/tcg-platform && cd /opt/tcg-platform
git clone https://github.com/ExzenTCG/tcg-platform.git .
# Compose lives at apps/server/docker-compose.yml after the monorepo move.
# It uses `name: tcg-platform` so volumes/networks stay `tcg-platform_*`
# regardless of cwd, but the runbook standard is to invoke from apps/server/.
cd apps/server
# Place the staging .env here (gitignored). Required keys: every Phase 3
# key (DATABASE_URL, JWT_SECRET, COOKIE_SECRET, SHOPEE_*) plus:
# - COOKIE_DOMAIN=.exzentcg.com (dashboard-staging can forward cookies)
# - PUBLIC_API_BASE_URL=https://tcg-staging.exzentcg.com (Phase 7 Shopee URLs)
docker compose up -d --build
docker compose ps
docker compose logs --tail 100 medusa

First build is slow

docker compose up -d --build takes ~2–3 minutes on first run because the builder stage runs yarn install + yarn medusa build to produce the production admin bundle. Subsequent runs without code changes reuse the Docker layer cache and are much faster. Rebuild only when source changes (pull + --build).

Pre-monorepo deploys (history)

Before Phase 5 PR #51 (2026-05-09), Medusa lived at the repo root and the runbook was cd /opt/tcg-platform && docker compose up. The Postgres volume from those deploys is named tcg-platform_postgres-data. The name: tcg-platform lock in the new compose preserves that exact volume name so existing data is not orphaned. Do not docker compose down -v on a host that has been initialised pre-monorepo unless you intend to wipe the DB.

Expected containers (from ExzenTCG/tcg-platform/docker-compose.yml):

  • medusa — Node.js 20 + Yarn Berry, exposes 192.168.0.55:9000
  • postgres — Postgres 16, container-internal only (port not published)
  • redis — Redis 7, container-internal only (port not published)

First-boot checks:

  • Entrypoint runs yarn medusa db:migrate then yarn medusa db:sync-links before medusa start. Migrations are idempotent — ~30s first boot creating ~150 tables, a no-op after.
  • Once logs show Server is ready on port: 9000, the admin UI is at http://192.168.0.55:9000/app (direct LAN / Tailscale) or https://tcg-staging.exzentcg.com/app/ once the tunnel + NPM are in place.
  • Seed the initial Admin user: docker compose exec medusa yarn medusa user --email <admin-email> --password <password> (password rotated immediately after first login).

Step 4 — LXC Firewall

Create /etc/pve/firewall/105.fw on the Proxmox host:

[OPTIONS]
enable: 1

[RULES]
IN ACCEPT -source +edge_gw -p tcp -dport 9000      # Only edge-gateway can reach Medusa
IN ACCEPT -source +admin_desktop -p tcp -dport 22   # SSH from admin
IN ACCEPT -source +admin_desktop -p tcp -dport 9000 # Direct LAN access for dev
IN DROP                                              # Block all other inbound
OUT ACCEPT -dest +router_gw -p udp -dport 53        # DNS
OUT ACCEPT -dest +router_gw -p tcp -dport 53        # DNS
OUT ACCEPT -p udp -dport 53                          # Public DNS fallback (1.1.1.1)
OUT DROP -dest +lan_subnet                           # Lateral movement prevention
OUT ACCEPT -p tcp -dport 443                         # HTTPS — npm registry, Medusa upgrades, outbound APIs
OUT ACCEPT -p tcp -dport 80                          # HTTP fallback (apt, some image registries)

No LAN lateral rules needed yet

Phase 1 is foundation-only: Medusa talks to its own sidecar Postgres/Redis inside the container (Docker bridge network, no host exposure). Shopee/Lazada/Telegram connector outbound rules are added in Phase 3 and Phase 6 respectively — strict OUT ACCEPT -dest <marketplace-api-hostname> -p tcp -dport 443 additions. Do not pre-open.

Verify after boot:

# From inside CT 105

# 1. LAN lateral-movement should be blocked (OUT DROP +lan_subnet)
curl -s --connect-timeout 3 http://192.168.0.16 -o /dev/null -w "HTTP %{http_code}\n"
# Expected: HTTP 000 (LAN blocked)

# 2. Proxmox host should be blocked (same OUT DROP rule)
curl -s --connect-timeout 3 https://192.168.0.200:8006 -o /dev/null -w "HTTP %{http_code}\n"
# Expected: HTTP 000 (Proxmox blocked)

# 3. Medusa self-reachability — use LOOPBACK, not the LAN IP.
# iptables OUTPUT fires even for packets destined to this container's own
# LAN NIC IP (192.168.0.55), so testing via the LAN IP hits the +lan_subnet
# DROP before Medusa. Loopback avoids the issue entirely.
curl -s --connect-timeout 3 http://localhost:9000/health -o /dev/null -w "HTTP %{http_code}\n"
# Expected: HTTP 200 (Medusa reachable via loopback)

External reachability test — from CT 101 edge-gateway, proves NPM will be able to proxy traffic to this container:

pct enter 101
curl -s --connect-timeout 3 http://192.168.0.55:9000/health -o /dev/null -w "HTTP %{http_code}\n"
# Expected: HTTP 200 (+edge_gw is explicitly allowed inbound on 9000 by 105.fw)
exit

Step 5 — Edge-gateway firewall update

Edit /etc/pve/firewall/101.fw on the Proxmox host. Add before the OUT DROP -dest +lan_subnet line:

OUT ACCEPT -dest 192.168.0.55 -p tcp -dport 9000   # Proxy to tcg-staging Medusa

Step 6 — NPM Proxy Host

Open NPM admin: http://192.168.0.51:81 → Proxy Hosts → Add:

Field Value
Domain Names tcg-staging.exzentcg.com
Scheme http
Forward Hostname / IP 192.168.0.55
Forward Port 9000
Block Common Exploits
Websockets Support (Medusa admin uses WebSockets for real-time notifications and collaborative updates; required in production mode too)
SSL Certificate None (Cloudflare terminates TLS)

Step 7 — Cloudflare Tunnel Route

Cloudflare → Zero Trust → Networks → Tunnels → exzentcg-homelab → Published application routes → Add:

Field Value
Subdomain tcg-staging
Domain exzentcg.com
Path (empty)
Service Type HTTP
URL localhost:80
HTTP Host Header tcg-staging.exzentcg.com

Cloudflare auto-creates the DNS CNAME (proxied).

Step 8 — Cloudflare Access policy

Zero Trust → Access → Applications → Add → Self-hosted:

Field Value
Application name tcg-staging
Subdomain tcg-staging
Domain exzentcg.com
Session Duration 24 hours

Policy — team-access (new, broader than owner-only — D1/D2/D3 all need access during Phase 1+):

  • Action: Allow
  • Selector: Emails → (D1, D2, D3 emails — add to homelab/00_secrets/Credentials Index.md)

Step 8b — Dashboard static-asset Access bypass

For dashboard-staging.exzentcg.com, create a second, higher-precedence Access application for public static build assets:

Field Value
Application name tcg-dashboard-staging-static-assets
Subdomain dashboard-staging
Domain exzentcg.com
Paths /_next/static/*, /_next/image*, /favicon.ico
Policy Bypass / Everyone

This bypass applies only to content-hashed public assets, not to dashboard pages or Medusa API calls. It is required because the Next.js Worker serves SSR HTML and client bundles from the same hostname. If Access protects /_next/static/*, the browser receives a cross-origin Cloudflare Access redirect for module scripts and server-action assets; the page looks rendered but client-only widgets such as the /imports/new variant picker stay inert. More-specific Access apps win by path, so keep this above the catch-all dashboard Access app.

Step 9 — Seed first Admin user

After the app is up and before handing off to the team:

pct enter 105
cd /opt/tcg-platform
docker compose exec medusa yarn medusa user \
  --email admin@exzentcg.com \
  --password <temporary-bootstrap-password>

Log in once at https://tcg-staging.exzentcg.com/app, force password rotation, then invite the rest of the team through the admin UI.

Step 10 — Verification

  • [ ] https://tcg-staging.exzentcg.com/app → Cloudflare Access login → Medusa admin login page
  • [ ] Admin login succeeds with the seeded user
  • [ ] Admin UI shows empty products, locations, orders (expected — no seed data)
  • [ ] docker compose logs medusa shows no error lines
  • [ ] Postgres migrations applied: docker compose exec postgres psql -U medusa -d medusa -c "\dt" returns ~150+ tables

Step 11 — Snapshot

pct snapshot 105 initial-deploy --description "tcg-staging initial deployment — Medusa v2 + Postgres 16 + Redis 7"

Updates needed elsewhere (post-deploy)

  • [ ] Add tcg-staging to homelab/00_secrets/Credentials Index.md (SSH key, seeded admin pw, Postgres pw, Redis pw, team email allowlist)
  • [ ] Update homelab/01_planning/Phase 1/Architecture Diagram.md — add CT 105 to the IP address map and Mermaid diagram
  • [ ] Add CT 105 to homelab/02_setup_logs/Phase 1 Health Checks.md
  • [ ] Update Homepage services.yaml on CT 103 with a card for tcg-staging (internal LAN only — don't expose widget API key externally)
  • [ ] Update tcg-platform/phase-1-plan.md to flip this runbook from Planned to Deployed

Troubleshooting (to be filled in during deploy)

Problem Cause Fix
ghcr.io DNS lookup fails Router DNS pct set 105 --nameserver 1.1.1.1 from Proxmox host, reboot CT
REDIS_URL fallback warning on boot environment: in docker-compose.yml overrides env_file: (same issue as Phase 0 spike) Set REDIS_URL=redis://redis:6379 explicitly in the environment: block, not .env
Blank admin page with virtual:medusa/i18n or Failed to resolve import "/src/..." errors Container is running dev mode (medusa develop) and hit medusajs/medusa#14828 Container should be on production mode per tcg-platform#6. Confirm with docker exec tcg-platform-medusa ps -o args= -p 1 — expect node .../medusa start, not medusa develop. If someone flipped CMD to yarn dev for debugging, WORKDIR=/server avoids the bug; reverting to /app re-introduces it.
Migration fails on first boot Prior partial Postgres volume from failed boot docker compose down -v && docker compose up -d --build (safe in staging only)
First docker compose up -d --build takes 5+ minutes Builder stage running yarn install + yarn medusa build from scratch Expected on first run or after a dependency change. Subsequent rebuilds reuse Docker layer cache.
Rebuild ENOSPCs at image-extract step 20 GB rootfs full from prior BuildKit cache pct exec 105 -- docker system prune -af (NO --volumes) reclaims ~10 GB. See the warning block above for why --volumes is now off-limits.
Medusa crashloops on Cannot find module '@medusajs/medusa/link-modules' Yarn 4 demoted @medusajs/medusa out of root node_modules after the dashboard added new deps. db:sync-links fails, set -e in docker-entrypoint.sh kills the container, docker restarts, repeat every ~60s. Pin @medusajs/medusa (matching version) as a root devDependency in monorepo package.json — fixed in tcg-platform#67 on 2026-05-12. If a future deploy resurfaces it for a different @medusajs/* package, same one-line fix. Diagnose with node -e "require.resolve('@medusajs/medusa/link-modules')" inside the image (via docker run --rm --entrypoint sh tcg-platform-medusa:latest -c '...').
Crashloop on fresh DB with relation "tax_provider" does not exist (also: payment_provider, notification_provider, fulfillment_provider, currency, region_country) — even though db:migrate reports 164 successful migrations A custom-module migration ran medusa db:generate against a dev DB that was missing the core schema. The generator reads "no commerce tables exist" as "drop everything and re-create from this module's entities only" and emits a 1000+-line full schema-sync up() body that silently destroys other modules' tables right after they're created. (Fixed in tcg-platform#24.) Diagnose: enable ALTER SYSTEM SET log_statement='all'; SELECT pg_reload_conf(); on postgres, wipe + re-run migrate, then docker compose logs postgres \| grep -ciE 'drop table' — > 0 for a custom-module migration is the smoking gun. Fix: rewrite the offending migration so its up() only contains its own DDL changes. Always run yarn medusa db:migrate BEFORE yarn medusa db:generate <module> so the generator sees the real schema.

Shopee connector troubleshooting

Symptom Likely cause Resolution
Webhooks return 200 but no Medusa order signature_ok=false on the raw event Check SHOPEE_PARTNER_KEY in the active profile or credentials row; verify timezone/clock skew on CT 105
SkuNotFoundError on every webhook Shopee model_sku not set or doesn't match Medusa SKU Update model_sku on Shopee OR add the SKU to Medusa; then UPDATE shopee_raw_event SET processed=false WHERE id='...' to retry
Telegram alerts not arriving SHOPEE_ALERT_WEBHOOK_URL unset OR n8n workflow disabled Check env var; verify n8n CT 102 is reachable from CT 105; check n8n workflow is Active
Token refresh failed on SDK calls Refresh token expired (Shopee docs ~30 days) Open the dashboard at /connectors/shopee, select the active profile/environment, and click Reconnect.
Dynamic URLs return 404 or are blank in connector setup PUBLIC_API_BASE_URL unset or incorrect Ensure PUBLIC_API_BASE_URL matches the staging origin in .env and compose, e.g. https://tcg-staging.exzentcg.com.

Phase 6 — multi-environment Shopee

Applied + validated 2026-05-15 — Phase 6 originally stored sandbox + live Shopee credentials in shopee_environment_credentials (Postgres table; secrets AES-256-GCM-encrypted with a key derived from JWT_SECRET). Phase 7 migrated those rows into shopee_connector_profile and dropped the legacy table. Keep this section for historical path-routed URL behavior; use the Phase 7 section below for the current operator workflow. See Phase 6 Implementation Plan, Phase 6 Findings, and Phase 7 Findings.

Path-routed URLs

Each environment registers its own webhook + OAuth-callback URLs in its own Shopee Partner Center:

Env Webhook URL (register in Partner Center) OAuth callback URL
Sandbox https://tcg-staging.exzentcg.com/connectors/shopee/webhook/sandbox https://tcg-staging.exzentcg.com/connectors/shopee/oauth/callback/sandbox
Live https://tcg-staging.exzentcg.com/connectors/shopee/webhook/live https://tcg-staging.exzentcg.com/connectors/shopee/oauth/callback/live

Legacy /connectors/shopee/webhook (no env suffix) keeps working — it aliases to sandbox. Operators didn't need to re-register the existing Sandbox webhook URL when upgrading to Phase 6, but live URLs had to be registered separately in the live Partner Center. Phase 7's profile detail screen now shows the current per-profile URLs, including ?profile_id=....

Shopee Partner Center accepts the bare origin for the redirect URL

Empirical from 2026-05-13: Shopee Partner Center won't let you paste a redirect URL with a path — only the origin (https://tcg-staging.exzentcg.com). It then accepts any path under that origin at OAuth time. So the redirect-URL field in Partner Center is https://tcg-staging.exzentcg.com; the actual redirect_uri sent in the auth request includes the full env-aware path. This works because Shopee whitelists by origin, not exact match.

Adding live Shopee to staging (operator workflow)

  1. Create the Live Shopee Open Platform app (separate from the Sandbox app you already have). Note the live partner_id, partner_key, and push_partner_key.
  2. Register redirect URL in the Live Partner Center: https://tcg-staging.exzentcg.com (Shopee accepts only the bare origin — see note above).
  3. Open the dashboard at /connectors/shopee and click the Live tab.
  4. Save credentials in the form: paste the live partner_id, partner_key, push_partner_key, shop_id, region (GLOBAL for SG live), and the URLs from the table above. Click Save credentials.
  5. Click Connect to start the OAuth flow against your live shop. Authorize in Shopee, return to dashboard with ?status=connected.
  6. Click Switch to Live when you want live to become the active environment. (Inactive sandbox webhooks will now 200-drop until you switch back.)

SHOPEE_* env vars are now advisory

After PR-1 lands and the boot-seeder runs, the SHOPEE_* env vars on CT 105 (SHOPEE_PARTNER_ID, SHOPEE_PARTNER_KEY, SHOPEE_PUSH_PARTNER_KEY, SHOPEE_SHOP_ID, SHOPEE_REGION, SHOPEE_API_BASE_URL, SHOPEE_DEFAULT_LOCATION_ID, SHOPEE_PUSH_URL, SHOPEE_OAUTH_CALLBACK_URL, SHOPEE_OAUTH_RETURN_URL) are only read on first-ever boot to seed the sandbox row. Subsequent reads come from the encrypted DB row.

To rotate sandbox credentials, edit them via the dashboard — not via .env. Editing .env post-seed has no effect on running behavior.

SHOPEE_OAUTH_RETURN_URL (the URL the OAuth callback redirects back to after token exchange) and JWT_SECRET (the source of the AES key for credential encryption) remain env-var-driven — they're infrastructure-level config, not connector credentials.

Don't edit legacy credential rows via raw SQL

Direct UPDATE … WHERE environment = '<env>' against the old Phase 6 table appeared to land, but the Medusa service kept reading the pre-update value until the credential cache was invalidated. Discovered during the 2026-05-15 smoke-test cleanup: a manual UPDATE … SET partner_id = NULL showed in psql immediately but the /admin/dashboard/connectors/shopee?env=live endpoint kept returning the prior value. Mikro-ORM's identity map across requests is the suspect; the Worker-side fetch is cache: 'no-store' so it's not the dashboard.

The supported edit path is the dashboard's profile UI — profile save / activation / reconnect endpoints invalidate the relevant SDK cache after writes. Operators using the UI never hit this. Only direct DB edits do.

If you must UPDATE connector rows directly (e.g., a maintenance script), restart Medusa afterwards (docker compose restart medusa) to force-clear all caches before the next request hits.

JWT_SECRET rotation caveat

The encrypted secret columns (partner_key_encrypted, push_partner_key_encrypted, access_token_encrypted, refresh_token_encrypted) are decryptable only with the JWT_SECRET they were written under. Rotating JWT_SECRET without re-encrypting the rows leaves the connector unable to verify webhook signatures or make outbound API calls. Phase 6 / 7 do not ship rotation tooling; if you need to rotate:

  1. Read the existing rows + decrypt with the old JWT_SECRET (docker compose exec medusa node -e "..." snippet)
  2. Set the new JWT_SECRET in .env + compose
  3. PATCH the secrets back via the dashboard (which re-encrypts under the new key)
  4. Restart Medusa

Plan a maintenance window — webhooks during steps 2–3 will fail signature verification.

Phase 7 — Shopee Connector Overhaul

Applied + validated 2026-05-16 — The connector now uses an N-profile model.

  1. Environment variable: PUBLIC_API_BASE_URL is mandatory. It drives dynamic push and OAuth callback URLs:
  2. Webhook: ${PUBLIC_API_BASE_URL}/connectors/shopee/webhook?env=${env_type}&profile_id=${profileId}
  3. OAuth callback: ${PUBLIC_API_BASE_URL}/connectors/shopee/oauth/callback/${env_type}?profile_id=${profileId}
  4. Profile management: Use the dashboard to manage shopee_connector_profile rows. Each profile identifies a region, partner, and shop tuple. Exactly one profile per env_type is active.
  5. Location selection: Use the Refresh Locations button in the profile UI to sync warehouse IDs from Shopee into shopee_warehouse_location for dropdown selection.
  6. Advisory locks: Proactive token refresh is serialized via Postgres advisory locks to prevent refresh-token rotation races.
  7. Cutover state: the migration window is closed on current tcg-platform/main. Migration20260516220000_drop_legacy_credentials_and_plaintext_tokens re-synced any drift from shopee_environment_credentials, dropped that legacy table, and dropped plaintext shopee_auth_token.access_token / refresh_token. Current token bytes live only in access_token_encrypted and refresh_token_encrypted; diagnostics expose metadata only.