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
| Flag | Default | Gates | When to flip |
|---|---|---|---|
PAYMENTS_ONLINE_ENABLED | false | UPI buttons everywhere, /api/payments/initiate* returns 503, /api/store-orders rejects paymentMethod=online | When company bank account opens (Razorpay/PhonePe onboarding requires it) — 2-3 weeks post-launch |
REELS_COMMERCE_ENABLED | false | Product/store tag carousel in viewer, "Tag products" entry in mobile post flow, /api/reels/[id]/tags PUT returns 503 | When commerce volume justifies the surface complexity |
Source of truth
src/lib/feature-flags.ts exports:
isOnlinePaymentsEnabled()— server-side checkisReelsCommerceEnabled()— server-side checkgetPublicFeatureFlags()— 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
- API:
/api/payments/initiate,/api/payments/initiate-storereturn 503 with helpful message - API:
/api/store-ordersPOST rejectspaymentMethod=onlinewith 400 - Web UI: cart + stores pages hide "Pay via UPI" button via
paymentsOnlineEnabledstate - Mobile UI: store screen hides UPI button via existing
paymentEnabledcheck (which now reflects the flag)
REELS_COMMERCE_ENABLED
- API:
/api/reels/[id]/tagsPUT returns 503 - Web viewer: ProductTagCarousel + legacy product/restaurant cards hidden
- Mobile post: "Tag products" entry hidden in post flow
- Mobile viewer: tag overlay filtered to empty when off
Pickup auto-default (related rule)
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
- Add to
src/lib/feature-flags.ts:export function isMyNewFeatureEnabled(): boolean {return readBoolEnv("MY_NEW_FEATURE_ENABLED", false);} - Use it server-side:
if (!isMyNewFeatureEnabled()) return NextResponse.json({error: "..."}, {status: 503}); - Optionally expose via
getPublicFeatureFlags()for client-side gating - Document in this page (add a row to "Active flags" table)
- Add a test in
tests/offline-only-launch.test.ts(or a new file following the same pattern) - Default to
falsealways — 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.