Phase 6 Kickoff Plan — Multi-environment Shopee Toggle¶
Status: design locked + PR-1 through PR-4 merged (2026-05-14)
Phase 6 ships a per-Medusa sandbox/live Shopee toggle. Phase 6 is the gateway feature that lets the merchant move from sandbox-only validation (Phase 3 cascade) to operator-controlled live cutover — without SSH, without env edits, without docker rebuilds. The single-Medusa-instance scope deliberately defers true sandbox/live infrastructure separation (a second CT for prod) to a future phase; that's a topology choice, not an integration one.
Why Phase 6¶
Phase 5 closed out with the operator dashboard rendering real Shopee sandbox data via a single set of SHOPEE_* env vars on CT 105. Switching to live Shopee — a separate Open Platform app with its own partner_id / partner_key / push_partner_key / shop_id — required:
- SSH into CT 105
- Edit
apps/server/.envto replace the six SHOPEE_ values - Edit
apps/server/docker-compose.ymlto plumb the same vars docker compose up -d --build(2–3 min)- Hit
/connectors/shopee→ Reconnect → authorize via live Partner Center
That dance was acceptable for one-time bootstrap. It is unacceptable for an operator who:
- Wants to test live Shopee for a feature without touching whatever the "real" production state is
- Has a degraded sandbox token and wants to flip to live to keep operating
- Onboards a future live Shopee app and needs to validate it before committing
The user's mental model — captured during scoping — is a login/logout switcher:
"Hmm actually it should be like a logon logout thing. logout of sandbox, doesn't mean it shld wipe the sandbox data. then if i login using live, then live orders should coexist with historical sandbox orders."
That model is what Phase 6 implements.
Design decisions¶
The following were scoped via direct conversation with the merchant during 2026-05-14 and locked in before code.
1. Both environments' data coexist in one Postgres¶
Sandbox and live Shopee data — auth tokens, raw events, order sync, exceptions — share a single Postgres database tagged by environment column. Switching the active environment does not wipe data; it only changes:
- Which environment's credentials the SDK uses for outbound API calls
- Which environment's webhooks are processed inbound
- Which environment's stats are surfaced in operator dashboards (active-env-only default; "all envs" toggle planned for PR-6)
Implication: the dashboard's /orders, /exceptions, /inventory views will eventually need env filters (PR-6 scope). For now they show all-envs union; sandbox orders mixed with live orders is the explicit tradeoff for "minimum useful".
2. Single-Medusa, internal toggle (not two Medusas)¶
Two viable architectures were considered:
| Option | Pros | Cons |
|---|---|---|
| (A) One Medusa, internal sandbox/live toggle | One CT to deploy; minimum useful in ~1.5 days; sandbox + live tokens live side-by-side | Inventory + order tables shared; tagging required to differentiate; operator must remember the active env |
| (B) Two Medusa instances (CT 105 sandbox, CT 106 prod) | True isolation — sandbox orders never touch prod DB; dashboard navbar switcher | ~3 days infra; need to provision CT 106 + tunnel + CF Access policy; no value until merchant has live Shopee creds |
Locked: (A) now. (B) becomes interesting once the merchant has actual live Shopee credentials and wants real production separation. (B) is "(A) twice with a navbar switcher in front" and remains a future infrastructure phase; Phase 7 ultimately stayed on the single Medusa instance and hardened the Shopee connector model instead.
3. Inactive-environment webhook semantics¶
When a webhook arrives at the inactive environment's path (/connectors/shopee/webhook/sandbox while live is active):
- Return 200 OK immediately (so Shopee stops retrying)
- Log + drop — no DB writes, no exception row, no Telegram alert
The alternative — persist inactive-env events as inactive status — was considered and rejected because:
- Inactive env's tokens may be expired anyway, so any processing attempt would fail downstream
- Accumulating inactive-env rows pollutes the table with non-actionable noise
- The operator can always see Shopee's Partner Center for raw event records if they need forensics
4. OAuth Connect button is disabled until credentials complete¶
The dashboard's "Connect Shopee" button stays disabled until partner_id + partner_key + shop_id are all set for the env being configured. Clicking with incomplete creds would 500 against Shopee's auth endpoint anyway — failing client-side with an obvious "save creds first" is cleaner UX.
5. Home-page stats are active-env-scoped by default¶
The home page cards (Open exceptions / Orders today / Pending fulfillment) count only the active environment's rows by default. A small "All envs" toggle in the corner shows the union. Rationale: the count an operator reads should reflect "what I need to triage right now," not "what sandbox happened to do last week."
(PR-4 scope did not include the home-page filter — captured as PR-6 follow-up.)
6. Credentials stored in Postgres, encrypted at rest¶
Both sandbox + live credentials live in a new table shopee_environment_credentials. The two high-value secrets — partner_key and push_partner_key — are stored as AES-256-GCM ciphertext with the key derived via HKDF-SHA256(JWT_SECRET).
Tradeoff captured in scoping:
- Plaintext-in-
.env(the Phase 5 state) is more secure against DB read attacks: the secret never reaches Postgres - DB-stored + encrypted enables UI-editable credentials, which the merchant explicitly chose for ergonomics
- Encryption narrows the exposure to "DB read AND JWT_SECRET read" instead of just "DB read"
JWT_SECRET rotation is not automated — rotating it without re-encrypting the rows would brick the secrets. Phase 6 shipped without rotation; Phase 7 also keys OAuth token encryption on JWT_SECRET, so rotation tooling remains an open maintenance-window follow-up ("if you must rotate, decrypt-all → rotate → re-encrypt-all").
7. Path-routed webhook + OAuth URLs¶
Each environment gets its own Partner Center registration:
- Sandbox:
https://<host>/connectors/shopee/webhook/sandbox+/oauth/callback/sandbox - Live:
https://<host>/connectors/shopee/webhook/live+/oauth/callback/live
This lets HMAC verification pick the right key from the env tag in the URL (no guesswork). Legacy single-env URLs (/webhook and /oauth/callback) alias to sandbox for backward compatibility — existing Shopee Partner Center sandbox registrations keep working without re-config.
Out of scope for Phase 6¶
- Multi-shop: a single (env, shop_id) pair per environment. Multi-shop would need its own
shopee_shop_credentialstable keyed by (env, shop_id) and is a separate phase. - Lazada / Telegram credential editors: the
/connectorsindex page lists them as "Not configured." Phase 6 only ships the Shopee tabs. - JWT_SECRET rotation tooling: documented as a manual procedure if needed.
- Env-filtered /orders /exceptions /inventory tables: PR-6 — env badges + filter dropdowns on the list pages, plus active-env-scoped home-page stats.
- Audit trail of who flipped active env when: out of scope; operator history is not modeled. Phase 5's
assigned_bypattern (on staff roles) is the analog — Phase 6 doesn't introduce a connector-action audit log.
PR sequence¶
| PR | Scope | Risk |
|---|---|---|
| PR-1 (#75) | Schema + migration + boot-seeder + AES-256-GCM crypto helper | Zero — additive only, seeds sandbox row from existing env vars |
| PR-2 (#77) | Env-aware SDK factory + getActiveEnvironment / setActiveEnvironment / getCredentialsForEnvironment on the service |
Zero — backward-compatible (single-arg getShopeeSdk(service) defaults to active env) |
| PR-3 (#78) | Path-routed /webhook/:env + /oauth/callback/:env; legacy URLs alias to sandbox; admin endpoints accept ?env= |
Low — existing webhook paths keep working unchanged |
| PR-4 (#79) | Two-tab dashboard UI; per-tab editable credentials form; active-env banner + Switch button; PATCH + active-environment admin endpoints | Medium — many dashboard surfaces touched; operator-facing |
| PR-5 (this doc) | Runbook updates + Partner Center setup notes + Phase 6 doc set | Zero — docs |
Implementation references¶
- Phase 6 Implementation Plan — task-by-task breakdown of each PR
- Server module:
apps/server/src/modules/connectors/shopee/— particularlycrypto.ts,service.ts,sdk.ts, and the newwebhook/handler.ts/oauth/callback-handler.ts - Dashboard:
apps/dashboard/app/(shell)/connectors/shopee/— tabbedpage.tsx, thecredentials-form.tsxclient component, and the three server actions inactions.ts - Schema:
apps/server/src/modules/connectors/shopee/migrations/Migration20260514120000.ts+ the matching shared-module migration
Operator runbook impact¶
Per-env Shopee Partner Center setup, staging redeployment notes, and the new env-aware URLs are captured in tcg-staging runbook → Phase 6 multi-env setup. When the merchant gets actual live Shopee credentials, that section is the canonical step-by-step.