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, exposes192.168.0.55:9000postgres— 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:migratethenyarn medusa db:sync-linksbeforemedusa 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 athttp://192.168.0.55:9000/app(direct LAN / Tailscale) orhttps://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 medusashows 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-stagingtohomelab/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.yamlon CT 103 with a card for tcg-staging (internal LAN only — don't expose widget API key externally) - [ ] Update
tcg-platform/phase-1-plan.mdto 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)¶
- Create the Live Shopee Open Platform app (separate from the Sandbox app you already have). Note the live
partner_id,partner_key, andpush_partner_key. - Register redirect URL in the Live Partner Center:
https://tcg-staging.exzentcg.com(Shopee accepts only the bare origin — see note above). - Open the dashboard at
/connectors/shopeeand click the Live tab. - Save credentials in the form: paste the live partner_id, partner_key, push_partner_key, shop_id, region (
GLOBALfor SG live), and the URLs from the table above. Click Save credentials. - Click Connect to start the OAuth flow against your live shop. Authorize in Shopee, return to dashboard with
?status=connected. - 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:
- Read the existing rows + decrypt with the old
JWT_SECRET(docker compose exec medusa node -e "..."snippet) - Set the new
JWT_SECRETin.env+ compose - PATCH the secrets back via the dashboard (which re-encrypts under the new key)
- 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.
- Environment variable:
PUBLIC_API_BASE_URLis mandatory. It drives dynamic push and OAuth callback URLs: - Webhook:
${PUBLIC_API_BASE_URL}/connectors/shopee/webhook?env=${env_type}&profile_id=${profileId} - OAuth callback:
${PUBLIC_API_BASE_URL}/connectors/shopee/oauth/callback/${env_type}?profile_id=${profileId} - Profile management: Use the dashboard to manage
shopee_connector_profilerows. Each profile identifies a region, partner, and shop tuple. Exactly one profile perenv_typeis active. - Location selection: Use the Refresh Locations button in the profile UI to sync warehouse IDs from Shopee into
shopee_warehouse_locationfor dropdown selection. - Advisory locks: Proactive token refresh is serialized via Postgres advisory locks to prevent refresh-token rotation races.
- Cutover state: the migration window is closed on current
tcg-platform/main.Migration20260516220000_drop_legacy_credentials_and_plaintext_tokensre-synced any drift fromshopee_environment_credentials, dropped that legacy table, and dropped plaintextshopee_auth_token.access_token/refresh_token. Current token bytes live only inaccess_token_encryptedandrefresh_token_encrypted; diagnostics expose metadata only.