Files
zeekay 1d8454b827 docs: say /v1/ where the docs mean our own API
Left at /api/, because these are not ours to rename:
- the vendor endpoint catalogues under products/warehouse_sources
- the ingress route table in docs/internal/feature-flags, which really does
  send /api/* to Django
- ActiveCampaign's own /api/3 path segment
- the billing service's routes in the Vercel sequence diagrams
- the Slack event and callback URLs, which are registered in the Slack app
  config and have to byte-match what the code still builds
2026-08-08 18:40:35 -07:00

10 KiB

Django API endpoints

Django serves the admin/management API for feature flags: CRUD operations, analytics, and organization-level operations. Runtime flag evaluation (/flags, /decide) is routed directly to the Rust service by Contour/Envoy at the Kubernetes infrastructure level -- these requests never reach Django. Django does make internal service-to-service HTTP calls to the Rust service for actions like my_flags and evaluation_reasons.

The /api/feature_flag/local_evaluation endpoint was historically served by a dedicated Django deployment (insights-local-evaluation). All local evaluation traffic is now served by the Rust definitions fleet at /flags/definitions (see Rust service overview). The Django endpoint and deployment have been removed.

Architecture overview

┌─────────────────────────────────────────────────────────────────┐
│                        Django API                               │
│          (routed via Contour from /v1/* paths)                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Project-scoped (CRUD + management)                             │
│  /v1/projects/{id}/feature_flags/                              │
│                                                                 │
│  Organization-scoped (cross-team ops)                           │
│  /v1/organizations/{id}/feature_flags/                         │
│                                                                 │
│  Legacy (derives team from user session)                        │
│  /v1/feature_flag/                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
         │                          │
         ▼                          ▼
   ┌──────────┐              ┌──────────────┐
   │ Postgres │              │ Rust flags   │
   │ (CRUD)   │              │ service      │
   │          │              │ (internal    │
   │          │              │  proxy)      │
   └──────────┘              └──────────────┘

Viewsets

All in insights/api/feature_flag.py unless noted otherwise.

Viewset Route Notes
FeatureFlagViewSet api/projects/{id}/feature_flags/ Primary viewset. Full CRUD, soft delete via ForbidDestroyModel.
LegacyFeatureFlagViewSet api/feature_flag/ Inherits FeatureFlagViewSet, derives team from session.
OrganizationFeatureFlagView api/organizations/{id}/feature_flags/ Cross-team operations. File: insights/api/organization_feature_flag.py.
FlagValueViewSet api/projects/{id}/flag_value/ Returns possible values for a flag. File: insights/api/flag_value.py.

Endpoint reference

CRUD operations

Standard REST on /v1/projects/{id}/feature_flags/. Hard DELETE is blocked — use PATCH with deleted: true for soft delete.

Custom actions

Method URL Description
GET .../feature_flags/my_flags/ All flags with values for the current user (proxied to Rust service)
GET .../feature_flags/evaluation_reasons/ Evaluate flags for a distinct_id with match reasons (proxied to Rust service)
POST .../feature_flags/user_blast_radius/ Estimate how many users a condition affects
POST .../feature_flags/{pk}/create_static_cohort_for_flag/ Create a static cohort from matched users
GET .../feature_flags/{pk}/status/ Flag status (ACTIVE, STALE, DELETED, UNKNOWN)
GET .../feature_flags/{pk}/dependent_flags/ Flags that depend on this flag
POST .../feature_flags/{pk}/dashboard/ Create a usage dashboard for the flag

Organization endpoints

Method URL Description
GET /v1/organizations/{id}/feature_flags/{key}/ Get a flag by key across all accessible teams
POST /v1/organizations/{id}/feature_flags/copy_flags/ Copy a flag from one project to target projects

copy_flags requires editor access to feature_flag in each target project, not just visibility. A caller who can see a project but can't edit flags there gets a failed entry for that target instead of a copy. target_project_ids is capped at MAX_COPY_FLAGS_TARGET_PROJECTS (50) per call, and the endpoint has its own burst/sustained throttles since each target project can create cohorts and a flag.

Key actions in detail

my_flags and evaluation_reasons

Both actions proxy to the Rust flags service via get_flags_from_service() in insights/api/services/flags_service.py. The Rust service URL defaults to http://localhost:3001 (configured via FEATURE_FLAGS_SERVICE_URL in insights/settings/data_stores.py).

create_static_cohort_for_flag

Creates a static cohort containing all users that match a flag's conditions. This is the only remaining use of the legacy Python flag matching code in insights/models/feature_flag/flag_matching.py.

Django model

FeatureFlag (insights/models/feature_flag/feature_flag.py)

Key things to know:

  • key is unique per team (UniqueConstraint(fields=["team", "key"]))
  • Hard deletes are blocked — deleted is a soft delete flag
  • version field provides optimistic concurrency control. Updates use select_for_update() and return HTTP 409 on version mismatch.
  • filters (JSONField) holds conditions, variants, payloads, and aggregation config
  • ensure_experience_continuity enables hash key overrides for consistent bucketing across identity changes
  • evaluation_runtime controls whether a flag is evaluated client-side, server-side, or both
  • The @approval_gate decorator on updates can require approval before changes take effect

Cache invalidation: The refresh_flag_cache_on_updates signal handler fires on save/delete, calling set_feature_flags_for_team_in_cache() via transaction.on_commit().

Model Purpose
FeatureFlagHashKeyOverride Experience continuity hash key storage (managed = False, handled by Rust migrations)
FeatureFlagDashboards Through table for flag <-> dashboard M2M
FeatureFlagEvaluationTag Links flags to evaluation context tags

Supporting modules

File Purpose
insights/models/feature_flag/flag_matching.py Legacy Python evaluation engine (only used for static cohort creation)
insights/models/feature_flag/flags_cache.py HyperCache for the Rust flags service with signal-based invalidation
insights/models/feature_flag/local_evaluation.py Prepares flag data for SDK local evaluation with HyperCache
insights/models/feature_flag/user_blast_radius.py Estimates user/group match counts for conditions
insights/api/services/flags_service.py HTTP proxy to the Rust flags service

Remote config endpoints

Remote config (/array/{token}/config, /array/{token}/config.js, /array/{token}/array.js) and the surveys config endpoint (/api/surveys) are no longer served by Django. They are served by the Rust hypercache service, which reads from the same RemoteConfig model populated by Django via post-save signals. See HyperCache system.

See also