Skip to main content

Slappcard — Feature Flags

Purpose: Protocol for gating surfaces that are not part of the Legacy Launch MVP. Default is OFF everywhere. Flags are PostHog-driven (client-side), with a DB-backed admin surface planned for Phase 2. Companions: STACK · ARCHITECTURE · MVP · ENV_CHECKLIST.md

Live status (2026-08-11): enforcement shipped to the stable URL (https://slappcard-staging.vercel.app) via merge staging → main (Vercel's production branch). /studio, /demo/*, /label/* render the "not enabled" fallback there.

Flags LIVE (2026-08-11): studio, demo_portals, label_portal, new_checkout are enabled (active, 100%) in the PostHog project so the staging preview exposes the gated surfaces. Note: PostHog's free plan (one project) + person-aggregation validation blocks URL-condition segmentation via API — per-env targeting needs device-mode conditions set in the PostHog dashboard, or a separate prod PostHog project (see PROD_TWIN.md).

PostHog is fully wired: NEXT_PUBLIC_POSTHOG_KEY on Vercel (all envs) + bws, personal API key in bws (POSTHOG_PERSONAL_API_KEY), session recording enabled (project level, maskAllInputs for privacy). The 4 flags (studio, demo_portals, label_portal, new_checkout) exist and are OFF (active: false — the release toggle). To enable one: Feature Flags → flag → flip the release toggle (and set rollout 100%). The client gate re-evaluates live via onFeatureFlags. Ship rule: develop on staging, merge to main to publish to the stable URL.


Registry (source of truth)​

apps/web/src/lib/features.ts — every flag must be declared here before use:

FlagKeyGates
studiostudioPrint studio surface (/studio, admin-linked)
demo_portalsdemo_portals/demo/* designer demos
label_portallabel_portal/label/* label portal
new_checkoutnew_checkoutFuture ordering / checkout flow
  • Defaults are OFF. Do not change the default; enable per environment in the PostHog dashboard instead.
  • flagKey(flag) maps a FeatureFlag name to its PostHog key string — never hardcode the string in views.

How surfaces are gated​

Use the client-side <FeatureGate> wrapper (render-time gate):

// app/studio/page.tsx
import Page from "@/views/StudioPage";
import { FeatureGate } from "@/components/FeatureGate";

export default function RoutePage() {
return (
<FeatureGate flag="studio">
<Page />
</FeatureGate>
);
}

Evaluation rules (all safe-by-default):

  1. NEXT_PUBLIC_POSTHOG_KEY / NEXT_PUBLIC_POSTHOG_HOST not set on Vercel → OFF.
  2. Flag not created, or not enabled for the environment → OFF.
  3. Server render / first paint → OFF (flips client-side after flags load).
  4. Flag on + loaded → surface renders.

Currently gated surfaces: /studio (studio), /demo/* (demo_portals), /label/* (label_portal).

Enabling a flag (staging)​

  1. Add NEXT_PUBLIC_POSTHOG_KEY + NEXT_PUBLIC_POSTHOG_HOST to Vercel (Preview/Development/Production; they are public by definition — create as non-sensitive so vercel pull can fetch them for local/CI builds). Staging PostHog project token lives in bws (NEXT_PUBLIC_POSTHOG_KEY) once provisioned.
  2. In the PostHog dashboard, create the flag (e.g. studio) and enable it for the relevant environment (staging: use the NEXT_PUBLIC_APP_ENV/host filter).
  3. Ship the flag off, verify the fallback panel, then flip in PostHog.

The client gate is enforcement for the Legacy Launch MVP. It is not a security boundary — treat it as UI gating. Do not rely on it for authorization (Convex authz is the real gate for domain ops).

Phase 2 (planned, not built)​

  • featureFlags table in Convex + admin UI. The AdminPortal Feature flags tab (/admin/feature-flags) is currently a ComingSoonPanel — it becomes the management surface (DB-driven with PostHog sync or fallback).
  • Server-side evaluation via @posthog/next (needs a server-side POSTHOG_API_KEY/POSTHOG_HOST env pair) for route middleware, and Convex-side checks for domain-critical actions.

Rules for AI agents​

  • New non-legacy surface → add a flag to lib/features.ts, wrap the route in <FeatureGate>, document it in the table above.
  • Never ship a surface whose flag is absent from the registry.
  • Never hardcode flag keys in views; use flagKey().
  • This file is allowlisted for the docs bot (update_docs.py) — keep it accurate when code changes.