feat(feature-flags): align with production contract, add seeding and SDK polling - #96
Merged
Merged
Conversation
Greptile SummaryThe PR aligns feature flags with the production-facing contract and makes them usable in seeded local environments.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
|
danielloader
added a commit
to danielloader/emulate
that referenced
this pull request
Sep 1, 2026
…null seed entries Review feedback on workos#96. `organizationIdsForUser` counted every membership regardless of status, so the user list endpoint reported flags inherited from an organization the user had been removed from or had not yet joined. authenticate gates organization scoping on `status === 'active'` in three places, so those flags could never reach that user's token claim — the list endpoint was the only surface reporting them. The featureFlags validator dereferenced each entry before checking its shape, so a YAML list item left empty (parsing as null) threw a raw TypeError out of `--validate-config` instead of reporting a path-based error. The same crash exists for `users` and `organizations` on main; left alone here as out of scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…SDK polling
Feature flags were the one emulated product with no way to create data — production
has no create-flag endpoint, and the emulator had no seed key — so the subsystem was
unusable for local development regardless of endpoint coverage.
Contract:
- Flag objects carry the spec's exact key set: adds `owner` and `tags`, drops the
non-production `type`, narrows `default_value` to boolean.
- ID prefixes `ff_`/`ff_target` become `flag_`/`flag_target`, as production emits.
- enable/disable move to PUT and target creation to POST returning 204, per the spec
and the Node SDK. The previous verbs stay as emulator-only aliases.
- Org and user endpoints return a paginated FlagList of whole Flag objects rather than
an ad-hoc `{slug,type,value,enabled}` shape no SDK can deserialize; the user endpoint
inherits targets from the user's organizations, as documented.
- Adds 400 `invalid_resource_id_format` and the spec's 404s on both target routes.
Seeding:
- New `featureFlags` seed key with targets joined to users by email and organizations
by name, validated for pinned-id uniqueness, duplicate targets and unresolvable
references before the emulator starts.
Evaluation:
- One rule shared by the `feature_flags` token claim, the list endpoints and the poll
endpoint, matching the SDK evaluator: disabled is off for everyone, otherwise a
matching enabled target wins, otherwise `default_value`.
SDK polling:
- Implements `GET /sdk/feature-flags`, which the SDK runtime client polls behind
`createRuntimeClient()`/`isEnabled()`. It is absent from the OpenAPI spec, so
spec-derived coverage cannot surface it; without it the runtime client never leaves
its bootstrap state.
Events:
- Flag payloads gain `environment_id`; `flag.rule_updated` is emitted on target changes
with `access_type`, `configured_targets` and `previous_attributes`.
Verified against @workos-inc/node 10.13.0: listUserFeatureFlags,
listOrganizationFeatureFlags, enable/disable, add/removeFlagTarget, and the runtime
client's polling, local evaluation and change events all work unmodified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…null seed entries Review feedback on workos#96. `organizationIdsForUser` counted every membership regardless of status, so the user list endpoint reported flags inherited from an organization the user had been removed from or had not yet joined. authenticate gates organization scoping on `status === 'active'` in three places, so those flags could never reach that user's token claim — the list endpoint was the only surface reporting them. The featureFlags validator dereferenced each entry before checking its shape, so a YAML list item left empty (parsing as null) threw a raw TypeError out of `--validate-config` instead of reporting a path-based error. The same crash exists for `users` and `organizations` on main; left alone here as out of scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-ups on workos#96. The Flag key set was hand-copied into the formatter and again into a test, so a spec change to `Flag` would drift past the conformance harness that guards every other resource. The generated shape catalog now owns it, envelopes included. `flag.rule_updated` reported a placeholder actor even though the target routes know the calling key, and it is the one flag event emitted with a request in hand. Inventing a value the emulator can recover contradicts the no-fabrication rule, and Vault already resolves `updated_by` from the same key record. Its `previous_attributes.data` likewise restated the flag's unchanged attributes as "previous", so only the rule context is reported now. Deleting a user or organization left its flag targets behind, where they could never be removed over the API (the target routes 404 on the missing resource first) and surfaced as a blank email in `configured_targets`. A slug needing percent-encoding seeded fine, but no route could ever address it, and a non-string organization target was reported as an unknown name rather than a type error.
gjtorikian
force-pushed
the
feat/feature-flags-parity
branch
from
September 3, 2026 18:54
c4f8c35 to
c27aa36
Compare
Contributor
Author
|
Wonderful fixes |
Collaborator
|
thanks! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #95.
Feature flags were the one emulated product with no way to create data. Endpoint coverage read ✅ 4/4 because the spec's four GETs existed, but nothing could put a flag in the store, the two evaluation endpoints returned a shape no SDK deserializes, and the endpoint the Node SDK's runtime client actually polls was unimplemented. This makes the subsystem usable for local development.
Context: we are adopting WorkOS feature flagging and run our test suite and local environments against this emulator, so this is on the path to adoption rather than a cleanup.
Contract
Flagobjecttypefield, noowner/tags,default_value: unknownff_…flag_…, as production emitsPOSTonlyPUT(spec + SDK),POSTkept as an aliasPUT+{value}body → 200/201 + bodyPOST, no body →204;PUTkept as an alias{slug, type, value, enabled}, unpaginatedFlagListof wholeFlagobjects, enabled-only400 invalid_resource_id_format,404for unknown flag/user/org on bothTargeting is now membership rather than assignment, matching production: the create route carries no body, so a target turns a flag on and cannot turn one off.
Seeding
New
featureFlagsseed key — the only way a flag can exist, since production has no create-flag endpoint:Validated before the emulator starts: pinned-id uniqueness and charset, duplicate slugs, duplicate targets, unresolvable user/organization references, and field types. A non-array
targets.usersreports a validation error rather than throwing aTypeErrorout of--validate-config.GET /sdk/feature-flagsworkos.featureFlags.createRuntimeClient()— the client behindisEnabled()andgetAllFlags()— does not use either list endpoint. It pollsGET /sdk/feature-flags, which is not in@workos/openapi-spec; I found it by reading the compiled SDK. Without itwaitUntilReady()never resolves.Because it is undocumented, spec-derived coverage cannot see it. The
SUPPORTED.mdnote now calls it out explicitly so the table does not imply SDK support it would not have.Evaluation
One rule behind all three surfaces, transcribed from the SDK's own
Evaluator.evaluate(): a disabled flag is off for everyone; otherwise a matching enabled target wins; otherwisedefault_value.The token claim and the user list endpoint are deliberately scoped differently — the claim covers the session's organization, the list endpoint every organization the user belongs to — which is how production scopes them.
seed-feature-flags.spec.tspins that divergence with a two-organization user rather than leaving it as an undocumented surprise.Events
Flag payloads gain
environment_id.flag.rule_updated— previously in the generated catalog but never emitted — now fires on target changes withaccess_type,configured_targetsandprevious_attributes.access_type/configured_targetsare scoped to that event only, since the spec does not define them on the other three. This required a small optionalcontextfield onWorkOSEvent; flag events are the only ones that populate it so far.Verification
Against the real
@workos-inc/node@10.13.0, not only the emulator's own tests:bun test: 1010 pass. One pre-existing unrelated failure,normalizeRedirectHost > rejects patterns that look plausible…, reproduces identically on unmodifiedmain(a punycode/ICU difference in the local Node build) and is untouched here.SUPPORTED.mdregenerates clean: Feature Flags goes ✅ 4/4 ·featureFlags; 157 → 160 endpoints overall.Breaking changes
Pre-1.0, but worth being explicit:
WorkOSFeatureFlagdropstypeand gainsowner/tags;default_valuenarrows toboolean.WorkOSFlagTargetdropsvalueand gainsenabled. Callers inserting flags directly viagetWorkOSStore()need updating — there was no other way to create one, so that is the whole affected population.ff_…literals will need to change.POSTon enable/disable andPUTon target creation still work, so callers on the old verbs are unaffected. Both are documented as emulator-only, since production rejects them.Not addressed
Two adjacent issues found while reviewing, both pre-existing and out of scope here:
cursorPaginate'sbeforecursor returns the head of the list rather than the page preceding the cursor, affecting every paginated endpoint.limit/orderquery parameters are coerced rather than validated, so no endpoint produces the spec's400 invalid_request_parameters.Happy to split either into its own PR.
🤖 Generated with Claude Code