Skip to content

feat(flagd): Flagd plugin#7753

Draft
aepfli wants to merge 19 commits into
Flagsmith:mainfrom
aepfli:feat/flagd-sync-endpoint
Draft

feat(flagd): Flagd plugin#7753
aepfli wants to merge 19 commits into
Flagsmith:mainfrom
aepfli:feat/flagd-sync-endpoint

Conversation

@aepfli

@aepfli aepfli commented Jun 11, 2026

Copy link
Copy Markdown

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

This adds the possibility to utilize flagsmith as an UI for flagd

How did you test this code?

See the documentation.

If the flagd plugin is activated, there is an endpoint providing a flagd like configuration. There are some caveats, eg. there is no regex possibility in flagd. Maybe we need to more clearly structure this in the documentation.

aepfli and others added 16 commits May 8, 2026 12:05
Exposes Flagsmith environments as flagd flag-definition documents at
/api/v1/flagd/flags.json so flagd can use Flagsmith as its sync source
and management UI.

- Translator layer: Flagsmith segment rules → JsonLogic, multivariate
  options → fractional, identity overrides → targetingKey checks.
- REGEX is skipped with a structured warning (no flagd equivalent).
- Server-side keys only; reuses existing Last-Modified pattern and
  adds a strong ETag for short-circuit polls.
- bootstrap_flagd_local management command for one-command local dev
  setup (org / project / env / server-side key, idempotent).
- Documentation covering both production integration and a SQLite-
  backed docker-compose stack with no manual key-minting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lead with Flagsmith-as-flagd-UI rather than Flagsmith-and-flagd
integration; spell out that the Flagsmith SDK is not in this
topology; drop the MD5-vs-MurmurHash bucketing aside (irrelevant if
you aren't mixing runtimes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds docker-compose.flagd-dev.yml plus updated docs. Brings up four
containers: Postgres, Flagsmith (UI + API), a one-shot bootstrap
container that mints the server-side key into a shared volume, and
flagd polling Flagsmith with that key. Single `docker compose up`,
no manual UI clicks to wire flagd up.

Switched from SQLite to Postgres after hitting a migration that uses
NOW() — Flagsmith isn't actually SQLite-compatible, only the URL
parser is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flagsmith's createinitialadminuser creates the admin with password=None
and only prints a reset link — DJANGO_ADMIN_PASSWORD is not a thing.
Extend the bootstrap command to also create-or-update an admin user
with a known password and attach it to the bootstrapped org, so the
local-dev compose stack truly works with one `docker compose up` and
no log-spelunking.

- bootstrap_flagd_local --admin-email / --admin-password (defaults
  admin@example.com / admin), always re-applied on each run for
  predictable local dev.
- Two new tests covering the admin path (fresh + refresh).
- Compose: drop the misleading ALLOW_ADMIN_INITIATION_VIA_CLI /
  ADMIN_EMAIL / DJANGO_ADMIN_PASSWORD env block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Flagsmith image ships a HEALTHCHECK using `flagsmith healthcheck
tcp`. The compose override that ran `wget -qO- /health/` never
succeeded — wget isn't in the Wolfi-base image — so the flagsmith
container stayed in health:starting forever and the bootstrap +
flagd init steps never fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Flagsmith image runs as `nobody`; named volumes are created
root-owned, so the bootstrap container can't write the server-key
env file into /shared. Re-introduce a small busybox init service
that fixes ownership before bootstrap runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream flagd image is distroless — no /bin/sh — so the compose
file couldn't `. /shared/flagd.env && exec flagd ...` to pick up the
server-side key emitted by the bootstrap container. flagd's YAML
config supports `authHeader` only (sets Authorization), not arbitrary
headers like X-Environment-Key, so we can't route around the shell
requirement that way either.

Solution: a 3-line Dockerfile that copies the flagd binary into
alpine. The compose builds it automatically on first `docker compose
up`, no separate step needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream flagd image ships its binary at /flagd-build, not /flagd
as I assumed. Local build confirmed: launcher image now builds and the
binary runs under the alpine shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the alpine launcher workaround with the right fix: flagd 0.13
dropped --sync-provider/--sync-provider-args in favour of a JSON
--sources array, whose HTTP provider only supports the Authorization
header for auth (via authHeader). So:

- EnvironmentKeyAuthentication now also reads the env key from the
  Authorization header (accepting both `Bearer <key>` and a raw token)
  so flagd can authenticate via its native authHeader field.
- bootstrap_flagd_local gains --api-key, pinning the EnvironmentAPIKey
  to a chosen value. The compose file uses compose interpolation
  (FLAGSMITH_SERVER_KEY, defaulting to a fixed local-dev key) so the
  key is known at compose-parse time and no runtime file ferrying is
  needed.
- flagd uses --sources=[{...,"authHeader":"Bearer ..."}] directly with
  the upstream distroless image. The launcher Dockerfile is gone.

Tests: 9 bootstrap + 10 endpoint cases (incl. Bearer/raw Authorization
fallback) all passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- flagd-sync.md: switch from --sync-provider/--sync-provider-args to
  --sources JSON across the bash, docker-compose, and Helm examples.
  Add Authorization header to the endpoint reference table.
- flagd-local-dev.md: clarify that flagd uses authHeader (not
  X-Environment-Key) and update the unauthorized-troubleshooting note
  to reflect the bootstrap pin model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Variants are now {'control': <value>, ...}; 'off' is only emitted
when a segment or identity override with enabled=False needs a
destination. defaultVariant is always 'control'; flagd's state field
carries the enabled/disabled semantic.

Tests adjusted to match the new shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-change fix

Bundles the operational and authoring concerns that turned up after
the first round of end-to-end testing.

Per-project enablement
- New FlagdProjectConfiguration model + migration.
- Sync and diagnostics endpoints return 404 when not enabled
  (discoverable: "this integration isn't configured" vs "denied").
- Admin toggle exposed via FlagdProjectConfigurationViewSet using the
  standard ProjectIntegrationBaseViewSet convention. Registered via
  one-line projects_router.register() alongside other integrations.
- bootstrap_flagd_local enables it automatically so local-dev still
  works out of the box.

Diagnostics endpoint
- GET /api/v1/flagd/diagnostics.json returns a structured report of
  translation warnings per feature (type mismatches, REGEX skipped,
  identity-override cap hit, etc.).
- Same env-key auth as the sync endpoint; same per-project gate.

Type-mismatch detection
- New translators/type_check.py emits a warning when a flag's
  control / multivariate / segment-override / identity-override
  values land in different flagd typed-flag schemas (boolean / number
  / string / object / array).
- Wired into diagnose_environment so operators see the warning
  before a flagd consumer hits TYPE_MISMATCH at evaluation time.

Scheduled-change cache invalidation
- Last-Modified / ETag now derive from
  max(environment.updated_at, max(FeatureState.live_from <= now),
      max(EnvironmentFeatureVersion.live_from <= now)).
- Scheduled v1/v2 changes propagate to flagd as soon as poll-after-
  live_from fires, rather than being short-circuited by a stale 304.

Tests
- 138 passing, 4 skipped (the 4 are optional flagd-binary downloads).
- New: project-gate, admin-toggle, diagnostics endpoint, last-modified
  scheduled-change behaviour, type-mismatch detection (incl. segment +
  identity override coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Segment and identity overrides in Flagsmith carry their own typed
``feature_state_value``. Before this commit the translator collapsed
every enabled override to the parent flag's ``control`` variant and
every disabled override to ``off`` — silently discarding the override's
typed value. flagd targeting can only return a *variant name* (literal
values are interpreted as variant keys), so preserving the value
requires synthesising a per-override variant.

- New ``override_<segment-slug>`` / ``override_<identifier-slug>``
  variants minted for each enabled override whose value differs from
  control, collision-suffixed.
- ``_resolve_override_variant`` now routes to that synthesised name
  when present; disabled overrides still route to ``off`` (disabled
  wins over value); same-as-control overrides still route to
  ``control`` (no extra variant needed).
- ``_build_targeting`` prunes no-op branches where the override
  variant equals the fallback, so flags whose overrides happen to
  carry the control value still emit no targeting at all.

Six new tests in ``test_override_values.py`` cover: distinct segment
value, distinct identity value, value-equals-control no-op, disabled
override still routing to off, slugified segment names, and two
distinct overrides on one flag. Three pre-existing tests updated to
reflect the new no-op pruning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a segment/identity override with ``enabled=False`` routed
to a synthesised ``off`` variant carrying a type-zero value, which
required emitting the off variant on the flag and embedded an
inferred 'disabled' semantic that may not have matched what the user
configured.

flagd has no per-segment ``state`` concept — only values. We now
treat ``override.enabled`` as decorative for flagd consumers: only the
override's typed ``feature_state_value`` flows through. Users who want
"disabled for this segment" set the override value explicitly (e.g.
``false`` for boolean flags, ``""`` for string flags).

Translator changes:
- ``VARIANT_OFF`` constant removed; no off-variant synthesis.
- ``_resolve_override_variant`` and the disabled-override scanning
  pass removed.
- Disabled override whose value equals control now emits a
  ``disabled_override_no_op`` translation warning so the silent
  no-op is visible via the diagnostics endpoint.

Documentation updated in the module docstring. Test fixtures across
flag-translation, identity-overrides, override-values, and the
real-flagd evaluation suite updated to assert the new model (147
passing, 4 skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
flagd's ``\$evaluators`` block is a shared keyspace — definitions
there are referenced by name from many flags. Putting every segment
there made sense when project-scoped segments were the only kind,
but Flagsmith also allows feature-scoped segments (one ``Segment``
row per feature, often loosely named). Two flags can each have a
feature-scoped segment named "mail" with completely different rules,
and dumping both into the shared keyspace forces unhelpful slug
collisions in a document operators have to read.

The decision is now usage-count-driven, keyed by segment id (never
name):

- Segment referenced by **two or more** features → extract to
  ``\$evaluators``, reference by ``\$ref`` from each flag.
- Segment referenced by exactly **one** feature → inline its JsonLogic
  directly into the flag's ``targeting``. No name leakage into a
  global keyspace; same-named feature-scoped segments stay isolated.
- Segment referenced by **zero** features → omit entirely (pre-existing
  bug: we used to add unused segments to ``\$evaluators``).

Implementation:
- ``services.py::_count_segment_usage`` walks each segment's
  ``feature_states`` and counts distinct feature ids.
- ``services.py`` only registers ``segment_keys`` and ``evaluators``
  entries for shared segments.
- ``flag.py::_build_targeting`` switches on ``segment_keys.get()`` —
  shared: ``{"\$ref": key}``; single-use: the raw segment JsonLogic
  inlined directly.

New unit tests cover: single-use inlining, two-feature extraction,
same-named feature-scoped segments not colliding, the 1/2/3 usage
threshold, and schema validation of an inline document. Two existing
tests updated: the integration sync test asserts inline (was asserting
``\$evaluators``); the schema test attaches the segment to two features
so it still demonstrates the extraction path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the integration docs in line with everything that landed in
this branch:

- Quick-start adds the per-project opt-in step (PATCH the admin
  toggle) before minting keys.
- Document anatomy example now reflects the actual variant shape:
  ``control`` (no ``off``), ``variant_N`` for multivariate,
  ``override_<slug>`` for segment/identity overrides with distinct
  values. Adds a dedicated example of a flag with a segment override.
- New "Variant naming" and "Segment placement" sections explain the
  inline-vs-extract behaviour and why there's no ``off`` variant.
- Compatibility matrix updated: ``control`` instead of ``on``/``off``,
  single-use segments inlined, multi-use segments extracted.
- Endpoint reference notes the new ``404`` status for projects where
  the integration isn't enabled, and the scheduled-change-aware
  ``Last-Modified`` / ``ETag``.
- New "Diagnostics endpoint" section covering the URL, response shape,
  and per-warning reasons (including ``type_mismatch`` and
  ``disabled_override_no_op``).
- New "Admin REST endpoint" section pointing operators at
  ``/api/v1/projects/<id>/integrations/flagd/``.
- Local-dev guide note updated: bootstrap now also enables the
  integration on the project it provisions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

@aepfli is attempting to deploy a commit to the Flagsmith Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added api Issue related to the REST API docs Documentation updates labels Jun 11, 2026
@aepfli aepfli changed the title Plugin: Flagd endpoint feat: Flagd plugin Jun 11, 2026
@aepfli aepfli changed the title feat: Flagd plugin feat(flagd): Flagd plugin Jun 11, 2026

def authenticate(self, request): # type: ignore[no-untyped-def]
api_key = request.META.get("HTTP_X_ENVIRONMENT_KEY")
if not api_key:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i feel like i need to revert this.

aepfli and others added 3 commits June 11, 2026 11:49
…at/flagd-sync-endpoint

# Conflicts:
#	api/poetry.lock
#	api/pyproject.toml
Upstream main enabled the C901 (max-complexity 10) ruff rule, which the
flagd translators exceeded. Split branch collection out of
_build_targeting, dispatch condition_to_jsonlogic through a per-operator
handler table, and hoist detect_type_mismatch's nested helpers to module
level. Also annotate the operator maps as dict[str, str] to fix the
Literal-key indexing mypy errors. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Issue related to the REST API docs Documentation updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant