Phase 6 Findings — Multi-environment Shopee Toggle¶
Closed out 2026-05-15
Phase 6 shipped across 6 PRs (#75 PR-1 schema, #77 PR-2 SDK factory, #78 PR-3 path-routed endpoints, #79 PR-4 dashboard UI + active-env switcher, #80 hotfix to seed empty Live row at boot, plus ExzenTCG-Homelab#39 docs). All deployed and validated on CT 105. See Phase 6 Kickoff Plan for the design and Implementation Plan for the PR breakdown.
What shipped¶
A login/logout switcher between Sandbox and Live Shopee credentials on a single Medusa instance. Operator can now configure both envs from dashboard-staging.exzentcg.com/connectors/shopee and flip the active env with one click — no SSH, no .env edits, no docker rebuild.
Behaviorally:
shopee_environment_credentialstable holds per-env credentials, withpartner_key+push_partner_keyAES-256-GCM-encrypted using a key derived fromJWT_SECRETshopee_active_environmentsingleton points at the currently-active env- Webhook routing is path-based (
/connectors/shopee/webhook/<env>); legacy/webhookaliases to sandbox so existing Partner Center registrations keep working - OAuth callbacks route per-env (
/connectors/shopee/oauth/callback/<env>) - All Shopee-touching data tables (
shopee_auth_token,shopee_raw_event,shopee_order_sync,connector_exception) carry anenvironmentcolumn; old rows backfilled tosandboxat migration time - Outbound SDK calls and inbound webhook signature verification both read credentials from the (decrypted) DB row, not from
process.env
Validation¶
Browser smoke (2026-05-15)¶
After deploying the Phase 6 image to CT 105 + applying the hotfix-cleanup PATCH, both tabs render cleanly in dashboard-staging.exzentcg.com:
- Sandbox tab: Access token state, fingerprints
shpk…5863·len64(partner_key) andaaaa…6x9s·len64(push_partner_key), regionTEST_GLOBAL, OAuth callback URL, push URL, default location ID. Reconnect button text legible (PR #74's contrast fix is in effect). Switch button correctly hidden (already the active env). - Live tab: empty form with helpful placeholders, "No token" badge, Connect + Switch buttons disabled with hint "Save complete credentials (partner_id, partner_key, shop_id) before connecting or switching."
- Active banner correctly identifies
Sandboxas the active env regardless of which tab is being viewed ACTIVEpill renders next to the right env name in the tab bar
Server smoke (curl + Python from CT 105)¶
PATCH + active-environment endpoints (6 tests):
- PATCH ?env=live round-trips partner_id ✓
- Secret round-trip: paste plaintext → DB stores ciphertext → GET returns fingerprint ✓
- Empty PATCH body → 400 ✓
- Switch to env with incomplete creds → 409 with Missing: shop_id message ✓
- No-op flip (already active) → 200 ✓
- Reads consistent with DB state ✓
Webhook routing (4 tests):
- Legacy /connectors/shopee/webhook + valid HMAC → 200, row tagged sandbox, signature_ok=true ✓
- Inactive /connectors/shopee/webhook/live + valid body → 200 + zero rows written in either env table ✓
- New env-aware /connectors/shopee/webhook/sandbox + valid HMAC → 200, row tagged sandbox, signature_ok=true ✓
- Bad signature on legacy path → 200, row written with signature_ok=false + connector_exception row ✓
OAuth callback routing (5 tests):
- /oauth/callback/sandbox?code=fake → 302 env=sandbox&status=error&reason=exchange_failed ✓
- /oauth/callback/live?code=fake → 302 env=live&status=error (live SDK init throws as expected) ✓
- /oauth/callback/sandbox (no code) → 302 reason=missing_code ✓
- /oauth/callback/garbage → 302 reason=bad_env_path ✓
- Legacy /oauth/callback (no env) → 302 aliased to env=sandbox ✓
Phase 3 cascade (regression):
Replayed a code=3 ORDER_STATUS_UPDATE for an existing sandbox order (260507S3QC5R2V). The full Phase 3 pipeline still fires under the Phase 6 image:
- HMAC verified ✓
- shopee_raw_event row inserted tagged sandbox, signature_ok=true ✓
- processEvent ran async; raw_event marked processed=true ✓
- SDK call to getOrderDetail succeeded against Shopee sandbox (token auto-refreshed) ✓
- shopee_order_sync.last_raw_event_id + shopee_status both updated ✓
- Discovered side-fact: that order has moved TO_CONFIRM_RECEIVE → COMPLETED on Shopee since the May 7 cascade test; the Phase 6 cascade correctly reconciled the state change
Gotchas + decisions captured in flight¶
Bug: ?env=live 500s on a fresh DB (caught at first deploy)¶
Initial PR-1 seeder only created the sandbox row. PR-4's dashboard fetches ?env=sandbox and ?env=live in parallel; the live request 500s because getCredentialsForEnvironment("live") throws "No credentials row" when none exists.
Fix in #80: the seeder also creates an empty live row (all credential fields null, region='GLOBAL'). The SDK still throws "not fully configured" if anyone tries to use the empty live creds, so there's no functional bypass — just a UI bootstrap that lets the cred form render.
Caching: direct SQL updates to shopee_environment_credentials are not immediately visible via the API¶
Discovered during smoke-test cleanup. After PATCHing test data into the live row, then running UPDATE … SET partner_id = NULL via psql, the dashboard kept showing the smoke-test partner_id value. The DB was empty; Medusa was returning a stale value. Cleared by PATCHing through the admin endpoint (which presumably refreshes the relevant cache layer — Mikro-ORM identity map across requests is the likely culprit; the Worker-side fetch is cache: 'no-store' so it's not the dashboard cache).
Implication for operators: always edit credentials through the dashboard's PATCH endpoint (the form's Save Credentials button), not direct SQL. The PATCH path invalidates the SDK cache and refreshes the read. Operators using the UI never hit this; only direct DB edits do.
Shopee Partner Center accepts only bare origin for redirect URL¶
Empirical from 2026-05-13 (caught during the Phase 5 OAuth setup, re-confirmed in Phase 6 Live setup): the redirect-URL field in Shopee Partner Center won't accept a path — only an origin. We send a full path-routed redirect_uri parameter in the OAuth request anyway; Shopee accepts any path under the whitelisted origin. Documented in the tcg-staging runbook → Phase 6 multi-environment Shopee section.
JWT_SECRET is now load-bearing¶
Pre-Phase-6, JWT_SECRET only authenticated Medusa session tokens. Post-Phase-6, it's also the source of the AES-256-GCM encryption key for partner_key_encrypted + push_partner_key_encrypted. Rotating it without re-encrypting those columns will brick the connector. The rotation procedure is sketched in the runbook but no automation ships in Phase 6 — it's a manual maintenance-window procedure if needed.
Open follow-ups¶
Closed¶
- Env badges + filter dropdowns on
/ordersand/exceptions— closed 2026-05-17 via #131. Both routes accept?env=sandbox|live; the dashboard filter forms surface an "Env" select between Status and the existing dropdowns./orders?env=…joins throughshopee_order_sync.environmentand implicitly drops non-Shopee orders (operator intent: "show me my sandbox/live universe"). Pre-Phase-6 null-tagged exception rows are silently excluded when the filter is set; absent param returns everything. The route fully paginatesshopee_order_sync1000 rows/batch — no arbitrary cap — so the result is correct at any merchant scale up to the SQL IN-clause ceiling (~30k rows in Postgres). Seeorder-sync-lookup.tsfor the helper. - Home-page stats default to active-env-only — closed 2026-05-17 via #131.
/admin/dashboard/summarydefaults togetActiveEnvironment()scope; pass?env=allfor cross-env totals or?env=sandbox|liveto override. Response carriesactive_environment+env_filter. The home page renders a pill above the cards (Counts scoped to <env> (active). Low-stock is cross-env regardless.) with a toggle to "View all envs". Click-throughs from cards propagate the env into the destination list-page URL.low_stock_itemsstays cross-env by design (inventory is physical).
Still open (Phase 7+)¶
- Two-Medusa-instance topology (CT 106 =
tcg-prod.exzentcg.com→ live Shopee). When the merchant has actual live Shopee credentials and wants real production separation (own DB, own auth tokens, own webhook event stream), spin up CT 106 + add a navbar env-switcher to the dashboard. Phase 6 was "single Medusa, internal toggle"; Phase 7 instead hardened the connector model on the single instance. True two-instance topology remains a future infra phase. (B) in the original architecture decision. JWT_SECRETrotation tooling — currently manual. Phase 7 also keys token encryption on it, so a rotation re-encrypts 4 columns across N rows; warrants a maintenance-mode admin endpoint + runbook.- Push the env→order-id join into a query.graph filter expression — current implementation collects all env-scoped sync rows into memory before filtering the order query. Fine at merchant scale (~10k–30k lifetime Shopee orders); if volume ever approaches the SQL IN-clause ceiling, switch to a server-side join through the Module Link. Captured for posterity in #131.
Superseded by Phase 7¶
- Multi-shop per environment — superseded by the Phase 7 N-profile model (#112 → #122). Multiple profiles can now exist per
env_type; exactly one is active per env at a time. The remaining constraint is operational, not schema-level: outbound calls and webhook verification resolve through the active/profile-selected shop.