Skip to main content

Feature Flags

Two production flags gate launch-time behaviour. Both default to false (safe / offline-only). Flip on Cloud Run when ready — no code change, no redeploy.

Active flags

FlagDefaultGatesWhen to flip
PAYMENTS_ONLINE_ENABLEDfalseUPI buttons everywhere, /api/payments/initiate* returns 503, /api/store-orders rejects paymentMethod=onlineWhen company bank account opens (Razorpay/PhonePe onboarding requires it) — 2-3 weeks post-launch
REELS_COMMERCE_ENABLEDfalseProduct/store tag carousel in viewer, "Tag products" entry in mobile post flow, /api/reels/[id]/tags PUT returns 503When commerce volume justifies the surface complexity

Source of truth

src/lib/feature-flags.ts exports:

  • isOnlinePaymentsEnabled() — server-side check
  • isReelsCommerceEnabled() — server-side check
  • getPublicFeatureFlags() — bundle for client API responses

The /api/payments/upi-config endpoint exposes both flags as paymentsOnlineEnabled + reelsCommerceEnabled to the frontend, plus an AND'd paymentEnabled field that means "online actually works right now" (provider configured AND flag on). Client code reads this in a single fetch.

Flip a flag on

gcloud run services update ka26-marketplace --region us-central1 --project=school-mgmt-saas \
--update-env-vars PAYMENTS_ONLINE_ENABLED=true

The next request to any container reads the new value. New cold-starts (the most common path) get it instantly; warm containers may still cache for ~30s depending on Node module-load timing.

Flip both at once (e.g., full launch)

gcloud run services update ka26-marketplace --region us-central1 --project=school-mgmt-saas \
--update-env-vars PAYMENTS_ONLINE_ENABLED=true,REELS_COMMERCE_ENABLED=true \
--update-secrets PHONEPE_MERCHANT_ID=phonepe-merchant-id:latest,PHONEPE_SALT_KEY=phonepe-salt:latest

(The --update-secrets is for when the bank/Razorpay onboarding gives you the gateway credentials.)

Defense in depth

Each flag is enforced at multiple layers:

PAYMENTS_ONLINE_ENABLED

  1. API: /api/payments/initiate, /api/payments/initiate-store return 503 with helpful message
  2. API: /api/store-orders POST rejects paymentMethod=online with 400
  3. Web UI: cart + stores pages hide "Pay via UPI" button via paymentsOnlineEnabled state
  4. Mobile UI: store screen hides UPI button via existing paymentEnabled check (which now reflects the flag)

REELS_COMMERCE_ENABLED

  1. API: /api/reels/[id]/tags PUT returns 503
  2. Web viewer: ProductTagCarousel + legacy product/restaurant cards hidden
  3. Mobile post: "Tag products" entry hidden in post flow
  4. Mobile viewer: tag overlay filtered to empty when off

When fulfillmentType is pickup_immediate or pickup_scheduled, the server FORCES paymentMethod=pay_at_pickup regardless of what the client sent. Single source of truth lives in /api/store-orders POST as resolvedPaymentMethod. Web UI also auto-snaps for nicer UX, but the server is the truth.

Why no NEXT_PUBLIC_*?

NEXT_PUBLIC_* env vars bake into the client bundle at build time. To flip them you'd have to rebuild + redeploy. Server-driven flags (read on every request) flip in seconds via env-var update. Worth the extra fetch.

Adding a new flag

  1. Add to src/lib/feature-flags.ts:
    export function isMyNewFeatureEnabled(): boolean {
    return readBoolEnv("MY_NEW_FEATURE_ENABLED", false);
    }
  2. Use it server-side: if (!isMyNewFeatureEnabled()) return NextResponse.json({error: "..."}, {status: 503});
  3. Optionally expose via getPublicFeatureFlags() for client-side gating
  4. Document in this page (add a row to "Active flags" table)
  5. Add a test in tests/offline-only-launch.test.ts (or a new file following the same pattern)
  6. Default to false always — your future self will thank you when you forget to set the env var on a new revision

Tests

tests/offline-only-launch.test.ts — 31 tests across 9 describe blocks. Locks both flags + their defense-in-depth points. Run: npm test -- offline-only-launch.