Skip to content

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:

  1. SSH into CT 105
  2. Edit apps/server/.env to replace the six SHOPEE_ values
  3. Edit apps/server/docker-compose.yml to plumb the same vars
  4. docker compose up -d --build (2–3 min)
  5. 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_credentials table keyed by (env, shop_id) and is a separate phase.
  • Lazada / Telegram credential editors: the /connectors index 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_by pattern (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/ — particularly crypto.ts, service.ts, sdk.ts, and the new webhook/handler.ts / oauth/callback-handler.ts
  • Dashboard: apps/dashboard/app/(shell)/connectors/shopee/ — tabbed page.tsx, the credentials-form.tsx client component, and the three server actions in actions.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.