# Roadmap What exists today, and what is planned. No dates — this is a statement of intent, not a commitment. Items move only when they are actually done. ## Shipped - **Feature flags per project and environment** — create, toggle, delete, with a per-project default state for new flags. - **Context-aware targeting** — ordered strategies with AND-ed constraints over typed context fields (string, enum, boolean, number, version, date). - **Admin UI** — React application covering projects, flags, environments, context fields, targeting, API keys and users. - **Role-based access control** — owner, admin, editor and viewer roles, with protected environments and a two-admin approval flow for production toggles. - **Three-tier API keys** — root admin, project admin and client keys, with optional TTL on admin keys. - **TypeScript SDK** — [`@flagraft/sdk`](../packages/sdk-js), with TTL caching, a cache size cap, stale-on-error fallback, request timeouts, negative caching for unknown flags, rate-limit backoff and per-call default values. - **Python SDK** — [`flagraft`](../packages/sdk-python), a sync, thread-safe, zero-dependency port of the TypeScript client with identical failure behaviour. - **Conditional requests** — `ETag` / `If-None-Match` on the bulk evaluation endpoint, so an unchanged flag list costs a 304 with no body. Handled automatically by the SDK. - **Import and export** — a versioned `flagraft.export` document that round-trips losslessly, plus a one-way importer for [Unleash](https://www.getunleash.io) exports and an Unleash-shaped export. Every import is previewable with `dryRun`, runs in one transaction, and reports anything it could not represent rather than approximating it — a strategy that cannot be carried over exactly is dropped, never widened. See [API.md](API.md#import-and-export). - **OpenAPI / Swagger UI** at `/docs`. ## Planned ### Evaluation - **Percentage rollouts** — deterministic bucketing on a context field so a flag can be enabled for a stable subset of users. The Unleash importer skips `flexibleRollout` below 100% today and will map it once this exists, so this is worth more than it looks: it is the single most common reason an Unleash migration comes out incomplete. - **Flag variants** — beyond boolean, returning a string or JSON value per matched strategy. The Unleash importer drops variants today and will map them once this exists. - **Constraint-level evaluation detail** — report _which_ constraint failed, so "why is this off for this user" is answerable without re-deriving the strategy by hand. ### Delivery and freshness - **SSE push invalidation** — let a flag change reach running SDKs immediately instead of on the next poll. The server already clears its own cache on every write, so the delay today is the SDK's `ttl` alone: up to 30 seconds by default. That is fine for a gradual rollout and poor for a kill switch, which is the case this is for. Design settled, not yet built: - `GET /api/v1/client/stream`, authenticated with the client key. That key is already scoped to one project and environment, so the key is the subscription — no subscribe protocol. - The event carries **invalidation, not data** (`{ environmentId, version }`). The server cannot push evaluated results because evaluation depends on each caller's context. The SDK drops the affected cache entries and refetches, and that refetch is a conditional request, so ETag support is what keeps it cheap. - The seven existing mutation sites that call `cache.delete`/`deleteByPrefix` collapse into one `flagsChanged(projectId, environmentId?)` decorator that clears and publishes together. - Opt-in in the SDK (`stream: true`), with reconnect backoff and a fall back to TTL polling whenever the stream is down. It must be an optimisation over today's behaviour, never a replacement for it. - Needs `LISTEN/NOTIFY` (below) to work on more than one server instance. - Not usable on Lambda, and awkward on Cloudflare Workers, since it needs a long-lived connection. The SDK compatibility table will need a note. - **Cross-instance cache invalidation via Postgres `LISTEN/NOTIFY`** — see Known limitations. Required before SSE can work behind a load balancer, and worth doing on its own merits. - **Configurable shared cache layer** — the flag-state cache is in-process only. BentoCache, already the cache in use, is a two-tier design: L1 is local memory, L2 is a shared store, and a bus keeps every instance's L1 in step. Only L1 is wired up today. What the operator would choose: - **Nothing** — today's behaviour, and the right answer for a single instance. Must stay the default: requiring Redis to run a flag server would be a poor trade. - **Redis as L2**, with the Redis bus for invalidation. The usual choice once more than one instance is running. - **A database table as L2**, via BentoCache's `database` driver, for deployments that will not add Redis. Slower than Redis and it needs a bus of its own, which is where `LISTEN/NOTIFY` fits — the two items are the same piece of work approached from either end. There is no third tier to configure: BentoCache stops at L1 and L2, so "Redis and the database" is a choice between them for the L2 slot, not a stack of both. Postgres is already a hard dependency and holds the truth anyway, so a miss at L2 falls through to a query, which is the layer a notional L3 would have been. The point of L2 is not speed — an in-memory hit is far quicker than a Redis round trip. It is that a cold or restarted instance warms from a shared store instead of hammering Postgres, and that invalidation reaches every instance at once. ### Operations - **Audit log** — who changed which flag, when, and in which environment. A stored record with an actor and a before/after, queryable from the admin UI and filterable by project, environment and actor. Not application logs: those answer "what happened to this request", and the question here is "who turned this on last Thursday". Writes only — flag and strategy changes, key creation and revocation, role and membership changes, approval decisions. Read traffic is deliberately excluded; the evaluation endpoint is polled on a timer and logging it would bury everything worth reading. - **SSO via OIDC** — sign in through an existing identity provider (Keycloak, Okta, Entra, Google Workspace) instead of managing a second set of passwords. Sketch: - Authorization code flow with PKCE, landing on the same session cookie the local login already issues, so nothing downstream of authentication changes. - IdP groups map to the existing owner / admin / editor / viewer roles, configured per deployment. Roles stay Flagraft's, not the IdP's. - Local accounts keep working alongside it. An SSO outage must never lock an operator out of their own kill switches. - SAML and SCIM provisioning are a separate, larger job and not part of this. - Not a paid tier. Charging for the login that makes a self-hosted tool usable in a company is the pattern this project exists to avoid. - **SAML and SCIM provisioning** — a separate, larger job than the OIDC flow above: SAML for identity providers that don't speak OIDC, SCIM for automated user provisioning and deprovisioning from the IdP instead of manual invites. Typically a hard requirement for larger enterprise buyers regardless of which SSO protocol they use. - **Custom RBAC roles** — today's four roles (owner/admin/editor/viewer) are fixed. Let a deployment define its own permission sets per project or environment for teams whose approval structure doesn't map onto four tiers. - **Webhooks on flag and audit events** — push flag changes and audit entries to an external endpoint (Slack, SIEM, internal notification service) instead of requiring a poll. - **Scheduled flag changes** — enable or disable a flag at a future time, for planned launches and time-boxed rollouts. - **Evaluation metrics** — optional reporting of which flags were evaluated, so stale flags can be found and removed. ### Security and compliance Flagraft is self-hosted, so SOC2/HIPAA-style certification is mostly the deploying organization's responsibility, not the project's. What the project can do is close the technical-control gaps a security review actually checks for, and document how the rest maps onto a deployer's own audit. - **MFA/2FA on login** — currently password (argon2) plus a session cookie only, no second factor. Expected by SOC2 CC6.1 and HIPAA's access-control safeguard for privileged accounts. - **Security response headers** — `@fastify/cors` is registered; `@fastify/helmet` is not (no CSP, HSTS, X-Frame-Options). - **Dependency and vulnerability scanning in CI** — `.github/workflows/` has no Dependabot config, CodeQL, or equivalent. A common ask in vendor security questionnaires. - **Independent security audit / pen test** — `SECURITY.md` already states plainly that Flagraft is pre-1.0 and has not had one. Usually the first thing a security review asks for. - **Encryption-at-rest documentation** — data lives in the customer's own Postgres, which is their responsibility, but there is no doc walking a HIPAA-covered deployer through configuring it (e.g. Postgres TDE / disk encryption) to meet the safeguard. - **Compliance control mapping doc** — a short published mapping from Flagraft's actual controls (RBAC, two-admin approval, scoped API keys, session cookie flags, and the audit log above once shipped) to SOC2 CC6 / HIPAA technical safeguards, so an enterprise security team can self-serve their questionnaire instead of asking for a certificate that does not apply to self-hosted OSS. Account lockout is a related, already-tracked gap — see [Known limitations](#known-limitations). ### Ecosystem - **React SDK** — `@flagraft/react`, a thin wrapper over the existing TypeScript client rather than a second implementation. A provider that hydrates the whole flag list in one call, a `useFlag(key, default)` hook that re-renders on change, and SSR support so the server-rendered markup and the first client render agree instead of flickering. The browser constraints drive the design: a client key shipped to a browser is public, so it must stay a client key scoped to one environment, and the evaluation context is whatever the page already knows about the user. - **Go SDK.** Any language can already call the HTTP API directly; this would add the same caching and fail-safe behaviour as the TypeScript and Python clients. Idiomatic where it matters — a `context.Context` on every call — but the same behaviour under failure, since an SDK that fails differently per language is worse than no SDK. - **Async Python client**, if asked for. The sync client works from asyncio through `asyncio.to_thread` today. - **PyPI release pipeline** for `flagraft`. - **npm release pipeline** for `@flagraft/sdk`. ## Known limitations - **Cache invalidation does not cross server instances.** The flag-state cache is in-process (`bentostore().useL1Layer(memoryDriver())`), with no shared layer and no bus. Every write clears the cache on the instance that handled it, so a single-server deployment is always consistent. Run two or more behind a load balancer and the other instances keep serving the previous state until their own entries expire — up to `CACHE_TTL_SECONDS`, 30 by default. Workaround today: run one instance, or lower `CACHE_TTL_SECONDS`. The fix is Postgres `LISTEN/NOTIFY`, which needs no new infrastructure since Postgres is already required, or a BentoCache bus over Redis for deployments that already run one — see Configurable shared cache layer. - **Regex constraints are screened, not sandboxed.** A `regex` pattern is rejected at write time when it has nested quantifiers, which covers the patterns that cause exponential backtracking by accident. The check is a star-height heuristic, so a deliberately ambiguous pattern such as `(a|a)*` still gets through, and patterns that are merely polynomial are allowed -- those stay cheap because the context values they run against arrive in a query string, which the HTTP layer already bounds. Closing the gap properly needs a matcher that can be given a timeout, which Node's `RegExp` cannot. - **Login throttling is per IP, with no account lockout.** An attacker spread across many IPs can still work through one account's password space slowly, and the limit counts successful logins alongside failed ones because `@fastify/rate-limit` cannot tell them apart. Lockout was left out on purpose: it needs failed-attempt state on the user row, an unlock path for the locked-out admin, and it hands anyone a way to lock a colleague out by guessing at their email. Worth doing, but it is a feature rather than a patch. ## Not planned - **Hosted SaaS.** Flagraft is self-hosted by design. - **Experimentation and analytics.** Flag evaluation is the scope; measuring outcomes belongs in your analytics stack. - **Automatic bulk fetching inside `isEnabled`.** Considered and rejected. Having a single flag check pull the whole catalogue helps an app with few distinct contexts and hurts one that passes per-user context, where every user would download every flag to answer one question. Calling `getFeatures(context)` once already gives the same benefit — later `isEnabled` calls with that context are answered from the cached bulk response with no request.