Skip to main content

kind: FeatureFlag

What it is

kind: FeatureFlag declares a runtime toggle that Crewship code checks at request time. A flag has two layers and the manifest can write to both in a single document:
  1. The instance-global definition — name (key), description, the default enabled state, and an optional default_percentage for gradual-rollout flags. This row lives in the feature_flags table and is shared by every workspace on the instance. Only ADMIN may create, update, or delete it.
  2. The workspace override — an optional per-workspace force-enable / force-disable that takes precedence over the instance default. The override row lives in feature_flag_overrides keyed by (flag_id, workspace_id). OWNER and ADMIN of the target workspace may write it.

Why two layers in one document

Most operational uses look like: “ship the flag definition with the codebase, then let individual workspaces opt-in (or stay opted-out) without round-tripping through the admin console.” Splitting the two concerns into separate kinds would force two YAML files and two PRs for every flag rollout. Keeping them in one document with an optional workspace_override keeps the surface tight and lets crewship apply reconcile both layers in one phase.

Instance default vs workspace override — concretely

The workspace_override field is pointer-typed in the Go struct (*bool). That is deliberate: an absent field means “inherit”, which is structurally different from “force OFF (false)”. Re-applying a manifest that omits the field will DELETE any stale override the workspace happened to have — that’s how operators clear an override and return to the instance default.

YAML schema

Field reference

Examples

Minimal — definition only

Defines an instance-global flag with no workspace-specific override. Every workspace inherits default_enabled.

Realistic — definition + workspace opt-in

Same flag, but the workspace this manifest applies to wants the feature ON regardless of the instance default. Two PlanItems will be emitted on first apply: POST the definition, then PUT the override.

Force-disable for a specific workspace

The instance default is true, but this workspace explicitly opts out — useful for a customer who isn’t ready for the change.

Clear an existing override

Drop the workspace_override: line entirely and re-apply. The plan emits one ActionDelete against DELETE /api/v1/feature-flags/<key>/override. The flag definition itself is untouched.

CLI reference

The override subcommands all operate on the current workspace (resolved via crewship workspace use / CREWSHIP_WORKSPACE); there is no --workspace flag for targeting an arbitrary other workspace from the CLI. To cross-target, switch workspace context first. For full CRUD (creating new flag definitions, deleting flags) use crewship apply --file flag.yaml. The admin CLI intentionally does NOT expose feature-flag create / delete — flag definitions are a versioned codebase concern and should land via a reviewed manifest, not an ad-hoc shell command.

REST endpoint mapping

DB columns: feature_flags(key, description, enabled, percentage) + feature_flag_overrides(flag_id, workspace_id, enabled). See internal/database/migrate_consts_v01_init.go for the schema.

Validation rules

  • metadata.slug is required (used as the server-side key).
  • spec.default_percentage MUST be in the closed range [0, 100]. Out-of-range values fail validation at parse time, before any REST call.
  • spec.workspace_override, when present, must be a YAML boolean (true / false). Pointer typing means “field omitted” is a third valid state and not a validation failure.
  • No cross-kind FK references — FeatureFlag is a leaf kind in the dependency graph. The WorkspaceContext argument to Validate is accepted for signature uniformity but ignored.

Apply behavior

Default mode (Upsert)

Plan() emits one PlanItem per drifted concern, with a maximum of two items per flag:
  • Flag definition missing remotely → ActionCreate (POST).
  • Flag definition present but description or default_enabled differ → ActionUpdate (PATCH).
  • Flag definition present and identical → no item.
  • workspace_override set in YAML and differs from remote (or remote has no override) → ActionUpdate (PUT override).
  • workspace_override absent in YAML and remote has an override row → ActionDelete (DELETE override).
  • workspace_override absent both sides → no item.
When the flag is being created AND the manifest sets an override, the POST runs first and the PUT second (declaration order); the override endpoint requires the flag to already exist. default_percentage is deliberately excluded from the update diff. Percentage rollouts are typically tuned at runtime by ops (“bump to 25%”); re-applying a manifest that hard-codes the original 0 would fight the live system on every CI run. The field is still honored on POST (for first-time bootstrap) and round-trips via export, but crewship apply won’t reset it once the row exists.

ApplyStrict

Fails with a clear error if the flag definition already exists. Use this in fresh-instance bootstrap pipelines where any pre-existing flag is a sign of a previous half-baked apply.

ApplyReplace

Emits ActionDelete for the flag definition first, then ActionCreate to recreate it from the manifest. Workspace override rows are cascaded-deleted by the foreign key on the feature_flags table, then recreated by the same Plan logic if the manifest declares an override. This mode is destructive — use --yes to bypass the interactive confirmation only after the dry run looks right.

Round-trip via export

crewship export workspace includes one kind: FeatureFlag document per flag definition the user can read. The workspace_override field is emitted ONLY when the server reports an override row for the current workspace (the JSON pointer workspace_override field is non-nil on the response). Pointer semantics carry through to the YAML: an absent field means “inherit the default.” Round-trip is lossless for description, default_enabled, default_percentage, and workspace_override. The feature_flags.id column (CUID) is regenerated on a fresh apply and is not preserved — references between kinds are always by slug, never by id.

See also

  • InstanceSetting — the other admin-only kind in the manifest; same dual-layer pattern but for configuration key/value pairs instead of feature toggles.
  • SPEC-2 §9 (.claude/context/specs/SPEC-2-manifest-complete.md) — the authoritative implementation contract.