Feature flag platform built for the edge. Ship features safely with targeting rules, percentage rollouts, and per-environment configuration.
Features ·
Getting Started ·
SDK ·
Tech Decisions ·
Testing ·
Observability
Live Website: switchflag.com
Switchflag gives you a full-stack feature flagging system: a dashboard to manage flags, an SDK to evaluate them, and observability to see what's happening. Flags are fetched once and evaluated client-side, so there's no network round-trip on every getFlag() call.
Most feature flag services are either expensive SaaS with per-seat pricing, or self-hosted tools with heavy infrastructure requirements. Switchflag is designed to run cheaply on edge infrastructure (Turso for the database, Vercel for compute) while still supporting the features teams actually need: targeting rules, gradual rollouts, multi-environment configs, and audit trails.
- Targeting rules — 10 operators (
equals,contains,startsWith,in,percent, etc.) evaluated in order, first match wins - Percentage rollouts — Consistent hashing (Java's
String.hashCode()algorithm) ensures the same user always lands in the same bucket - Multi-environment — Each flag has independent configuration per environment (dev, staging, production)
- Multi-tenant — Organization-based access control with owner/admin/member roles and email invitations
- Billing & plans — Stripe-powered subscription tiers (Developer, Startup, Business) with enforced limits on flags, requests, and team members
- Audit logging — Every flag change is recorded with who changed what and when
- Evaluation analytics — Daily aggregated counts per flag per environment, no massive event tables
- Full observability — Server-side OpenTelemetry traces + metrics, browser-side Grafana Faro RUM (Web Vitals, JS errors, page views)
- Accessibility — WCAG 2.1 AA compliance enforced via automated axe-core checks on every page
- Type-safe SDK — Zero-dependency, published on npm with standard, edge, Next.js, and React entry points
┌─────────────────────────────────────────────────────────────┐
│ Monorepo │
│ │
│ apps/ │
│ dashboard/ Admin UI (Next.js 15, React 19) │
│ docs/ Documentation site (Fumadocs) │
│ web/ Marketing site │
│ sandbox/ SDK integration demo │
│ │
│ packages/ │
│ sdk/ Client SDK (standard + edge variants) │
│ evaluator/ Rule engine + consistent hashing │
│ shared/ Zod schemas, types, test factories │
│ db/ Turso/Drizzle schema + repositories │
│ ui/ Component library (shadcn/ui) │
│ │
│ tooling/ │
│ eslint-config, typescript-config, tailwind-config │
└─────────────────────────────────────────────────────────────┘
The SDK ships in two variants:
- Standard — In-memory
Mapcache, suited for long-lived server processes - Edge — Uses
fetchwithcache: 'force-cache'andnext: { revalidate: 60 }, delegating caching to the edge network since edge functions are ephemeral
| Area | Choice | Why |
|---|---|---|
| Database | Turso (libSQL) + Drizzle ORM | SQLite at the edge with embedded replicas. Drizzle gives type-safe queries with zero runtime overhead |
| Auth | Better Auth | Handles session management, email verification, password reset, and org/member models without a separate auth service |
| Monorepo | Turborepo + pnpm | Topological task ordering (^build dependencies), cached builds, workspace protocol for internal packages |
| Validation | Zod + @t3-oss/env-nextjs | Zod schemas as single source of truth for runtime validators and TypeScript types. Environment variables validated at build time |
| Testing | Vitest + MSW + Playwright + axe-core | Vitest workspace across all packages. MSW for API mocking. Playwright for E2E with visual regression and WCAG 2.1 AA accessibility checks |
| Styling | Tailwind CSS 4 + shadcn/ui | Shared Tailwind preset across apps. shadcn gives copy-paste components without a runtime dependency |
| Observability | OpenTelemetry + Grafana Faro | OTel for server traces/metrics (every server action is wrapped in withSpan()), Faro for browser RUM. Both disabled when env vars are unset — zero overhead in local dev |
| Resend | Transactional emails for invitations, verification, and password reset. Opt-in via RESEND_API_KEY |
|
| Payments | Stripe | Subscription billing with webhook-driven plan enforcement. Three tiers with limits on flags, requests, and team members |
| Security | Rate limiting + security headers | Upstash Redis rate limiting on auth and API routes. Security headers (CSP, HSTS, X-Frame-Options) applied via middleware. API keys hashed before storage |
| Code quality | Husky + commitlint + Prettier + syncpack | Conventional commits enforced on every commit. Syncpack ensures consistent dependency versions across the monorepo |
| Releases | Changesets + GitHub Actions | Automated versioning and npm publishing for @switchflag/sdk. Bundle verification ensures no Zod leaks and evaluator is properly inlined |
- Node.js >= 20
- pnpm 9.15.2
- A Turso database (free tier works)
git clone https://github.com/thomasblaymire/switchflag.git
cd switchflag
pnpm install
cp .env.example apps/dashboard/.env.localFill in your .env.local:
| Variable | Required | What it does |
|---|---|---|
TURSO_DATABASE_URL |
Yes | Your Turso database URL |
TURSO_AUTH_TOKEN |
Yes | Turso auth token |
BETTER_AUTH_SECRET |
Yes | Encryption key for sessions (min 32 chars) |
BETTER_AUTH_URL |
Yes | http://localhost:3001 for local dev |
RESEND_API_KEY |
No | Enables transactional emails (invites, verification, password reset) |
STRIPE_SECRET_KEY |
No | Enables billing and subscription management |
STRIPE_WEBHOOK_SECRET |
No | Verifies incoming Stripe webhook events |
UPSTASH_REDIS_REST_URL |
No | Enables rate limiting on auth and API routes |
UPSTASH_REDIS_REST_TOKEN |
No | Auth token for Upstash Redis |
OTEL_EXPORTER_OTLP_ENDPOINT |
No | OTLP collector — enables server-side tracing and metrics |
OTEL_EXPORTER_OTLP_HEADERS |
No | Auth headers for your OTLP collector |
NEXT_PUBLIC_GRAFANA_FARO_URL |
No | Grafana Faro collector — enables browser RUM |
All optional integrations (email, billing, rate limiting, observability) gracefully degrade when their env vars are unset — local development requires only the four required variables.
pnpm dev # Dashboard on localhost:3001import { createClient } from '@switchflag/sdk'
const client = createClient({ apiKey: 'sf_live_...' })
const result = await client.getFlag('new-checkout-flow', {
userId: 'user-123',
attributes: {
plan: 'pro',
country: 'GB',
},
})
if (result.enabled) {
// new checkout
}For edge runtimes (Vercel Edge Functions, Cloudflare Workers):
import { createEdgeClient } from '@switchflag/sdk/edge'
const client = createEdgeClient({ apiKey: 'sf_live_...' })
// Same API, but caching is delegated to the edge network- SDK fetches the flag configuration once from the API
- Targeting rules are evaluated locally, in order — first match wins
- For percentage rollouts, the evaluator hashes
userId:ruleIdto produce a deterministic 0-99 bucket - The same user always gets the same result for the same rule (no flicker between variants)
- API key hashing — Keys are hashed before storage; only a prefix is retained for display
- Rate limiting — Upstash Redis-backed limits on SDK API (100 req/min), sign-in (10 req/min), and sign-up (5 req/min)
- Security headers — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy applied via Next.js middleware
- Environment validation — All env vars validated at build time via
@t3-oss/env-nextjswith Zod schemas
Every server action is wrapped in a traced span with custom attributes:
project.create { project.slug: "my-app" }
flag.toggle { flag.key: "new-checkout", environment: "production" }
flag.delete { flag.key: "old-feature" }
A custom flag.change.count metric tracks mutation frequency. The OTel SDK is initialized via Next.js instrumentation.ts hook, dynamically imported to avoid bundling Node.js code into edge functions.
Faro captures Web Vitals (LCP, INP, CLS, TTFB, FCP), JS errors, console errors, page views, and fetch timing. Browser traces propagate traceparent headers to server traces, giving you full request visibility from click to database.
Both layers are opt-in — unset the env vars and they produce zero overhead.
pnpm test # Unit tests across all packages
pnpm test:watch # Watch mode
# E2E
cd apps/dashboard
pnpm test:e2e # Headless Playwright
pnpm test:e2e:ui # Interactive UI mode- Vitest workspace runs tests across all packages in parallel
- MSW (Mock Service Worker) for network-level API mocking without coupling tests to implementation details
- Testing Library for component tests driven by user behavior, not implementation
- Factory builders (
createFlag(),createFlagEnvironment(),createTargetingRule(), etc.) so tests construct realistic data without boilerplate - Distribution tests verify hash uniformity across 1000 simulated users in the evaluator package
- Multi-persona testing — Pre-seeded test accounts (owner, member, admin) with distinct roles for role-specific assertions
- Functional tests — Auth flows, project CRUD, API key management, billing, team invitations, onboarding per role
- Accessibility tests — axe-core scans every page against WCAG 2.1 AA, violations fail the build
- Visual regression — Screenshot comparison against Linux baselines, auto-generated in CI
- Smoke tests — Critical path subset for fast feedback
Every PR gets an isolated Turso database with seeded test accounts. A GitHub Action creates the database, applies migrations, seeds users, and posts a PR comment with login credentials. When the PR is closed, the database is destroyed.
| Workflow | Trigger | What it does |
|---|---|---|
| CI | Push / PR | Lint, typecheck, unit tests, build, coverage upload to Codecov |
| E2E | Push / PR | Playwright tests (functional, a11y, screenshots), auto-commits baselines |
| Preview DB | PR open / close | Creates isolated Turso database per PR, seeds test accounts, posts credentials comment |
| Release | Push to main | Changesets versioning, SDK bundle verification (no Zod leaks, evaluator inlined), npm publish |
| Bundle size | PR | Analyzes JS bundle sizes for dashboard, web, and SDK, reports top 10 chunks |
pnpm dev # Start dev servers
pnpm build # Build all packages (topologically ordered)
pnpm test # Run tests
pnpm lint # Lint all packages
pnpm typecheck # TypeScript checks
pnpm format # Prettier formattingswitchflag/
├── apps/
│ ├── dashboard/ # Admin UI
│ │ ├── src/
│ │ │ ├── app/ # Next.js App Router (pages, layouts, error boundaries)
│ │ │ ├── actions/ # Server actions (traced with OTel spans)
│ │ │ ├── components/ # React components
│ │ │ └── lib/ # Auth, DB, telemetry, email, rate limiting
│ │ ├── e2e/ # Playwright tests (functional, a11y, screenshots)
│ │ └── grafana/ # Importable Grafana dashboard JSON
│ ├── docs/ # Documentation site (Fumadocs + MDX)
│ ├── web/ # Marketing homepage
│ └── sandbox/ # SDK demo app
├── packages/
│ ├── sdk/ # Standard + edge client SDKs (published on npm)
│ ├── evaluator/ # Targeting rules + percentage rollouts
│ ├── shared/ # Zod schemas, types, test factories
│ ├── db/ # Turso/Drizzle schema, repositories
│ └── ui/ # shadcn/ui component library
└── tooling/
├── eslint-config/
├── typescript-config/
└── tailwind-config/
Built with Next.js 15, React 19, Turso, Drizzle, and TypeScript



