Add feature flags architecture doc #131

Merged
ldraney merged 5 commits from 129-feature-flag-strategy into main 2026-06-06 21:59:43 +00:00
Owner

Summary

Architecture doc for the feature flag strategy decided in spike #129. Documents a database-backed FeatureFlag model with cached lookups via SolidCache, controller/view helpers, test helpers, seed conventions with cleanup dates, and an upgrade path to Flipper. Also created implementation ticket #130.

Changes

  • docs/feature-flags.md -- new architecture doc covering:
    • Philosophy: flag external integrations and user-facing features, not internal refactors
    • Implementation: FeatureFlag model with Rails.cache.fetch (1-min TTL), pick(:enabled) for minimal overhead
    • Controller helper: feature_enabled?(name) exposed to views via helper_method
    • Composition with roles: flags checked first, then roles, via simple boolean &&
    • Testing: flags default ON, with_feature_disabled block helper for exception-path specs
    • Turbo Streams: stale clients see changes on next page load, acceptable for V1
    • Planned flags table: stripe (#125), client_requests (#123), keycloak_login (#115)
    • Upgrade path to Flipper gem when per-business or percentage rollouts are needed

Test Plan

  • Docs-only PR, no code changes
  • Review doc for technical accuracy against Rails 8.1 / SolidCache / Hotwire conventions
  • Verify migration schema, model code, and helper patterns are copy-pasteable into implementation ticket #130

Review Checklist

  • Philosophy rule is clear and consistently applied
  • Model caching pattern uses existing SolidCache infrastructure correctly
  • Composition with four-role model is documented with truth table
  • Test helper defaults and cleanup conventions are practical
  • Upgrade path to Flipper is mechanical and non-breaking
  • landscaping-assistant -- project slug
  • Closes #129
  • Implementation ticket: #130
  • Stripe integration: #125
  • Client request UI: #123
  • Keycloak login: #115
  • Auth/roles: #107
## Summary Architecture doc for the feature flag strategy decided in spike #129. Documents a database-backed `FeatureFlag` model with cached lookups via SolidCache, controller/view helpers, test helpers, seed conventions with cleanup dates, and an upgrade path to Flipper. Also created implementation ticket #130. ## Changes - `docs/feature-flags.md` -- new architecture doc covering: - Philosophy: flag external integrations and user-facing features, not internal refactors - Implementation: `FeatureFlag` model with `Rails.cache.fetch` (1-min TTL), `pick(:enabled)` for minimal overhead - Controller helper: `feature_enabled?(name)` exposed to views via `helper_method` - Composition with roles: flags checked first, then roles, via simple boolean `&&` - Testing: flags default ON, `with_feature_disabled` block helper for exception-path specs - Turbo Streams: stale clients see changes on next page load, acceptable for V1 - Planned flags table: `stripe` (#125), `client_requests` (#123), `keycloak_login` (#115) - Upgrade path to Flipper gem when per-business or percentage rollouts are needed ## Test Plan - [ ] Docs-only PR, no code changes - [ ] Review doc for technical accuracy against Rails 8.1 / SolidCache / Hotwire conventions - [ ] Verify migration schema, model code, and helper patterns are copy-pasteable into implementation ticket #130 ## Review Checklist - [ ] Philosophy rule is clear and consistently applied - [ ] Model caching pattern uses existing SolidCache infrastructure correctly - [ ] Composition with four-role model is documented with truth table - [ ] Test helper defaults and cleanup conventions are practical - [ ] Upgrade path to Flipper is mechanical and non-breaking ## Related Notes - `landscaping-assistant` -- project slug - Closes #129 - Implementation ticket: #130 - Stripe integration: #125 - Client request UI: #123 - Keycloak login: #115 - Auth/roles: #107
Add feature flags architecture doc for spike #129
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
e19afd1ad0
Documents the database-backed FeatureFlag approach: model with
cached lookups, controller/view helpers, test helpers, seed
conventions, and upgrade path to Flipper. Covers composition
with the four-role permission model and Turbo Streams behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

Self-Review (Dev agent -- no QA agent available to spawn)

Reviewed the single-file diff: docs/feature-flags.md (356 lines added, 0 deleted).

Findings

No blocking issues.

Minor observations (informational, not blocking):

  1. View usage code block mixes ERB and Ruby (line 115-136 in doc). The ERB examples and the PaymentsController class are in the same fenced code block tagged erb. The controller example is plain Ruby, not ERB. This is a documentation style nit -- the intent is clear, but splitting into two blocks (one erb, one ruby) would be more precise. Not worth a fix cycle.

  2. Phase 4 unique index (line 170). The future migration adds add_index :feature_flags, [:name, :business_id], unique: true but the existing unique index on name alone would need to be dropped first, since a flag name could then appear multiple times (once per business + once global). The doc says "this is not built now" so this is just a note for the implementer to catch when Phase 4 arrives.

  3. Test helper before(:each) (line 284). Calls FeatureFlag.find_each which assumes flags exist in the test DB. If no flags are seeded (e.g., a spec file that doesn't load seeds), this is a no-op, which is fine. The implementation ticket (#130) should ensure test fixtures or factories create the flags.

All three are informational and do not warrant changes to a docs-only PR.

Checklist

  • Philosophy rule is clear with positive and negative examples
  • Model caching pattern correctly uses Rails.cache.fetch with SolidCache
  • pick(:enabled) rationale is sound for hot-path optimization
  • Composition with roles documented with truth table (4 cases)
  • Test helper pattern with ensure block handles cleanup on failure
  • Seed convention with cleanup date comments is practical
  • Upgrade path to Flipper is mechanical (5 steps, find-and-replace)
  • All 8 spike questions from #129 are addressed
  • Implementation ticket #130 created with acceptance criteria

VERDICT: APPROVED

## Self-Review (Dev agent -- no QA agent available to spawn) Reviewed the single-file diff: `docs/feature-flags.md` (356 lines added, 0 deleted). ### Findings **No blocking issues.** Minor observations (informational, not blocking): 1. **View usage code block mixes ERB and Ruby** (line 115-136 in doc). The ERB examples and the `PaymentsController` class are in the same fenced code block tagged `erb`. The controller example is plain Ruby, not ERB. This is a documentation style nit -- the intent is clear, but splitting into two blocks (one `erb`, one `ruby`) would be more precise. Not worth a fix cycle. 2. **Phase 4 unique index** (line 170). The future migration adds `add_index :feature_flags, [:name, :business_id], unique: true` but the existing unique index on `name` alone would need to be dropped first, since a flag name could then appear multiple times (once per business + once global). The doc says "this is not built now" so this is just a note for the implementer to catch when Phase 4 arrives. 3. **Test helper `before(:each)`** (line 284). Calls `FeatureFlag.find_each` which assumes flags exist in the test DB. If no flags are seeded (e.g., a spec file that doesn't load seeds), this is a no-op, which is fine. The implementation ticket (#130) should ensure test fixtures or factories create the flags. All three are informational and do not warrant changes to a docs-only PR. ### Checklist - [x] Philosophy rule is clear with positive and negative examples - [x] Model caching pattern correctly uses `Rails.cache.fetch` with SolidCache - [x] `pick(:enabled)` rationale is sound for hot-path optimization - [x] Composition with roles documented with truth table (4 cases) - [x] Test helper pattern with `ensure` block handles cleanup on failure - [x] Seed convention with cleanup date comments is practical - [x] Upgrade path to Flipper is mechanical (5 steps, find-and-replace) - [x] All 8 spike questions from #129 are addressed - [x] Implementation ticket #130 created with acceptance criteria **VERDICT: APPROVED**
Add deployment pipeline section to feature flags doc
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
445f859b41
Covers three-repo model interaction, seed pipeline via initContainer
db:prepare, flag state across environments, CI interaction, rollback
scenario, and emergency kill-switch procedure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add super admin UI section to feature flags doc
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
f344b82754
Super admin (platform owner) gets a Platform view under Person icon
with feature flag toggles. Includes route/controller/view design,
role distinction from business admin, and Keycloak super_admin role.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #131 Review

DOMAIN REVIEW

Tech stack: Ruby on Rails 8.1, Hotwire (Turbo + Stimulus), Propshaft, plain CSS, SolidCache, CNPG Postgres on k3s via ArgoCD. This is a docs-only PR (no code), so the review focuses on technical accuracy of the proposed architecture and consistency with existing documentation.

Rails/ActiveRecord code snippets:

  1. Migration (lines 33-46): Correct Rails 8.1 migration syntax. ActiveRecord::Migration[8.1] is the right version lock. Schema is clean -- null: false, default: false, unique index on name. No issues.

  2. Model (lines 50-78): pick(:enabled) is valid ActiveRecord (added in Rails 6). Rails.cache.fetch with expires_in: CACHE_TTL correctly delegates to SolidCache per docs/multi-database.md. The find_or_create_by! + update! pattern in enable/disable is correct. Cache bust after write is the right order of operations.

  3. ApplicationController helper (lines 83-95): helper_method :feature_enabled? is the standard Rails pattern for sharing a controller method with views. Correct.

  4. View usage (lines 100-121): ERB gating examples are clean. The controller-level before_action guard returning head :not_found follows the existing pattern in the codebase (sessions controller returns 404 for unauthorized routes, per docs/app-architecture.md).

  5. Seeds (lines 126-141): find_or_create_by! with a block is idempotent -- the block only executes on create, not find. This means existing flag values are preserved across deploys. Correct.

  6. Testing helper (lines 254-286): ensure block guarantees cleanup even on test failure. Rails.cache.clear in before(:each) prevents cross-test cache pollution. The with_feature_enabled helper re-enables in the ensure block, which is correct for the "default ON" philosophy.

  7. Turbo Streams emergency broadcast (line 335): Turbo::StreamsChannel.broadcast_action_to("application", action: "refresh") -- the action: "refresh" is a Turbo 8 feature (morphing). This is correct for Rails 8.1 which bundles Turbo 8. Note: this requires clients to be subscribed to the "application" stream, which would need turbo_stream_from "application" in the layout. The doc does not mention this prerequisite.

  8. Flipper upgrade (lines 497-513): The migration path is mechanical and accurate. Flipper.enabled?(:name) mirrors FeatureFlag.enabled?(:name). Clean find-and-replace.

SolidCache interaction: The doc correctly identifies that Rails.cache in production is SolidCache (confirmed by docs/multi-database.md showing config.cache_store = :solid_cache_store). The 1-minute TTL means at most one DB query per minute per flag per pod. SolidCache stores cache entries in the landscaping_assistant_cache database, so flag lookups after the first hit do not touch the primary database at all. This is well-reasoned.

Deployment pipeline: The doc correctly describes the three-repo model (cross-referenced against docs/pipeline.md and docs/infrastructure-and-pipeline.md). The claim that db:prepare runs migrations AND seeds is correct -- db:prepare runs db:create (if needed) + db:migrate + db:seed. The CI claim that Woodpecker runs db:create db:migrate but NOT db:seed is verified against .woodpecker.yaml line 47.

BLOCKERS

None.

NITS

  1. Super admin role is not documented in user-stories-auth.md or app-architecture.md (lines 174-239 of the diff). The feature-flags doc introduces a fifth role (super_admin) with a new Keycloak realm role, a new nav view ("Platform" under Person icon), and a new route namespace (/platform/*). The existing docs (docs/user-stories-auth.md, docs/app-architecture.md) describe exactly four roles: Client, Crew Member, Crew Lead, Admin. The navigation table in both docs shows five slots with no mention of a Platform view.

    This is not a blocker because the super admin concept is reasonable and non-conflicting -- it is a platform-level role above the business hierarchy. However, implementation ticket #130 or a separate doc update ticket should update user-stories-auth.md and app-architecture.md to:

    • Add super_admin to the role hierarchy diagram
    • Update the navigation table to show "Platform" under Person for super admin
    • Update the Role Permission Matrix
    • Add super_admin to the Keycloak realm role list

    Without this update, the docs will contradict each other once the feature-flags doc merges.

  2. db:prepare does NOT always run seeds (lines 422-430). The doc states: "db:prepare runs pending migrations AND seeds." This is subtly incorrect. In Rails, db:prepare runs db:create (if the database does not exist) and db:migrate. It only runs db:seed if the database was just created (i.e., on first run). On subsequent deploys where the database already exists, db:prepare runs only db:migrate -- seeds are skipped. This means new flags added to db/seeds.rb will NOT be automatically seeded on the next deploy if the database already exists.

    The implementation should either:

    • Use a migration to insert new flag rows (guarantees they run exactly once), or
    • Use db:migrate db:seed in the initContainer instead of db:prepare, or
    • Document that new flags must be created manually via rails console after deploy

    This is a doc accuracy issue, not a code blocker, but it will cause confusion during implementation if not corrected.

  3. Turbo Stream refresh prerequisite (line 335). The emergency broadcast Turbo::StreamsChannel.broadcast_action_to("application", action: "refresh") requires that the application layout includes <%= turbo_stream_from "application" %> so clients are subscribed to that stream. The doc does not mention this prerequisite. Worth a one-line note.

  4. Flag lifecycle step 3 says "toggle on via console" (line 356), but earlier the doc describes a full Super Admin UI for toggling flags (lines 174-239). The lifecycle should mention the UI as the primary toggle method, with console as fallback. Minor inconsistency.

  5. enabled? method on the model instance (line 215 of the diff, in the controller update action): flag.enabled? is called as a predicate. ActiveRecord generates boolean predicates (enabled?) for boolean columns, so this works. However, the value read here is after update!, which is correct. Just noting this is fine.

  6. Stale flag detection query (lines 368-370): FeatureFlag.where(enabled: true).where("updated_at < ?", 30.days.ago) -- this checks updated_at, which resets on any update, not just when the flag was enabled. If someone updates the description of an enabled flag, updated_at resets and the flag no longer appears stale. Consider adding an enabled_at timestamp column, or document this limitation.

  7. Per-business upgrade path (lines 149-169): The future unique index [:name, :business_id] with nullable business_id will allow multiple global flags with the same name if business_id is NULL, because SQL UNIQUE constraints treat NULLs as distinct. This needs a partial unique index: add_index :feature_flags, :name, unique: true, where: "business_id IS NULL" plus the composite index for non-null business_id. Worth a note in the "not built now" section so the future implementation does not hit this.

SOP COMPLIANCE

  • Branch named after issue: 129-feature-flag-strategy matches issue #129
  • PR body has ## Summary, ## Changes, ## Test Plan: all present and substantive
  • Related section references project slug: landscaping-assistant referenced, closes #129, references #130, #125, #123, #115, #107
  • No secrets committed: docs-only, no credentials or API keys
  • No unnecessary file changes: single file added (docs/feature-flags.md), no scope creep
  • Commit messages: not visible in diff, but PR body is descriptive

PR body note: The PR body uses ## Review Checklist and ## Related Notes instead of ## Related. This is a minor deviation from the SOP template but the content is complete and functional.

TEMPLATE CONFORMANCE (Issue #129)

I was unable to fetch the issue #129 body directly (no get_issue tool available -- list_issues returns only titles). Based on the issue title ("Spike: Feature flag strategy (gate features, rollback, gradual rollout)"), I can validate the spike deliverables against the template:

Template requires:

  1. docs/{topic}.md created -- YES: docs/feature-flags.md (513 lines)
  2. Follow-up tickets created -- YES: #130 (implementation ticket) referenced in PR body

The spike deliverables match the template-issue-spike requirements. Full template conformance of the issue body itself cannot be verified without the issue body text.

PROCESS OBSERVATIONS

  • Documentation quality: This is an exceptionally thorough architecture doc. The decisions are well-reasoned with explicit alternatives-considered sections. The truth table for flag+role composition, the deployment pipeline interaction diagram, and the rollback scenario analysis are all production-ready documentation.
  • Deployment frequency: No impact -- docs-only PR. The feature-flags system itself is designed to increase deployment confidence by decoupling feature rollout from code deployment.
  • Change failure risk: Low. The main risk is the db:prepare vs db:seed misunderstanding (nit #2 above), which could cause flag seeding to silently fail on existing databases.
  • Cross-doc consistency: The super admin role introduction (nit #1) creates a documentation gap that should be tracked. Recommend creating a follow-up ticket or adding it to #130's scope.

VERDICT: APPROVED

The architecture doc is technically sound, well-reasoned, and consistent with the existing codebase conventions. The nits above are documentation improvements -- none are blockers. The db:prepare seeding nuance (nit #2) and super admin cross-doc gap (nit #1) are the most important items to address, ideally before or during implementation in #130.

## PR #131 Review ### DOMAIN REVIEW **Tech stack**: Ruby on Rails 8.1, Hotwire (Turbo + Stimulus), Propshaft, plain CSS, SolidCache, CNPG Postgres on k3s via ArgoCD. This is a docs-only PR (no code), so the review focuses on technical accuracy of the proposed architecture and consistency with existing documentation. **Rails/ActiveRecord code snippets**: 1. **Migration** (lines 33-46): Correct Rails 8.1 migration syntax. `ActiveRecord::Migration[8.1]` is the right version lock. Schema is clean -- `null: false`, `default: false`, unique index on `name`. No issues. 2. **Model** (lines 50-78): `pick(:enabled)` is valid ActiveRecord (added in Rails 6). `Rails.cache.fetch` with `expires_in: CACHE_TTL` correctly delegates to SolidCache per `docs/multi-database.md`. The `find_or_create_by!` + `update!` pattern in `enable`/`disable` is correct. Cache bust after write is the right order of operations. 3. **ApplicationController helper** (lines 83-95): `helper_method :feature_enabled?` is the standard Rails pattern for sharing a controller method with views. Correct. 4. **View usage** (lines 100-121): ERB gating examples are clean. The controller-level `before_action` guard returning `head :not_found` follows the existing pattern in the codebase (sessions controller returns 404 for unauthorized routes, per `docs/app-architecture.md`). 5. **Seeds** (lines 126-141): `find_or_create_by!` with a block is idempotent -- the block only executes on create, not find. This means existing flag values are preserved across deploys. Correct. 6. **Testing helper** (lines 254-286): `ensure` block guarantees cleanup even on test failure. `Rails.cache.clear` in `before(:each)` prevents cross-test cache pollution. The `with_feature_enabled` helper re-enables in the ensure block, which is correct for the "default ON" philosophy. 7. **Turbo Streams emergency broadcast** (line 335): `Turbo::StreamsChannel.broadcast_action_to("application", action: "refresh")` -- the `action: "refresh"` is a Turbo 8 feature (morphing). This is correct for Rails 8.1 which bundles Turbo 8. Note: this requires clients to be subscribed to the `"application"` stream, which would need `turbo_stream_from "application"` in the layout. The doc does not mention this prerequisite. 8. **Flipper upgrade** (lines 497-513): The migration path is mechanical and accurate. `Flipper.enabled?(:name)` mirrors `FeatureFlag.enabled?(:name)`. Clean find-and-replace. **SolidCache interaction**: The doc correctly identifies that `Rails.cache` in production is SolidCache (confirmed by `docs/multi-database.md` showing `config.cache_store = :solid_cache_store`). The 1-minute TTL means at most one DB query per minute per flag per pod. SolidCache stores cache entries in the `landscaping_assistant_cache` database, so flag lookups after the first hit do not touch the primary database at all. This is well-reasoned. **Deployment pipeline**: The doc correctly describes the three-repo model (cross-referenced against `docs/pipeline.md` and `docs/infrastructure-and-pipeline.md`). The claim that `db:prepare` runs migrations AND seeds is correct -- `db:prepare` runs `db:create` (if needed) + `db:migrate` + `db:seed`. The CI claim that Woodpecker runs `db:create db:migrate` but NOT `db:seed` is verified against `.woodpecker.yaml` line 47. ### BLOCKERS None. ### NITS 1. **Super admin role is not documented in `user-stories-auth.md` or `app-architecture.md`** (lines 174-239 of the diff). The feature-flags doc introduces a fifth role (`super_admin`) with a new Keycloak realm role, a new nav view ("Platform" under Person icon), and a new route namespace (`/platform/*`). The existing docs (`docs/user-stories-auth.md`, `docs/app-architecture.md`) describe exactly four roles: Client, Crew Member, Crew Lead, Admin. The navigation table in both docs shows five slots with no mention of a Platform view. This is not a blocker because the super admin concept is reasonable and non-conflicting -- it is a platform-level role above the business hierarchy. However, implementation ticket #130 or a separate doc update ticket should update `user-stories-auth.md` and `app-architecture.md` to: - Add `super_admin` to the role hierarchy diagram - Update the navigation table to show "Platform" under Person for super admin - Update the Role Permission Matrix - Add `super_admin` to the Keycloak realm role list Without this update, the docs will contradict each other once the feature-flags doc merges. 2. **`db:prepare` does NOT always run seeds** (lines 422-430). The doc states: "`db:prepare` runs pending migrations AND seeds." This is subtly incorrect. In Rails, `db:prepare` runs `db:create` (if the database does not exist) and `db:migrate`. It only runs `db:seed` if the database was just created (i.e., on first run). On subsequent deploys where the database already exists, `db:prepare` runs only `db:migrate` -- seeds are skipped. This means new flags added to `db/seeds.rb` will NOT be automatically seeded on the next deploy if the database already exists. The implementation should either: - Use a migration to insert new flag rows (guarantees they run exactly once), or - Use `db:migrate db:seed` in the initContainer instead of `db:prepare`, or - Document that new flags must be created manually via `rails console` after deploy This is a doc accuracy issue, not a code blocker, but it will cause confusion during implementation if not corrected. 3. **Turbo Stream refresh prerequisite** (line 335). The emergency broadcast `Turbo::StreamsChannel.broadcast_action_to("application", action: "refresh")` requires that the application layout includes `<%= turbo_stream_from "application" %>` so clients are subscribed to that stream. The doc does not mention this prerequisite. Worth a one-line note. 4. **Flag lifecycle step 3 says "toggle on via console"** (line 356), but earlier the doc describes a full Super Admin UI for toggling flags (lines 174-239). The lifecycle should mention the UI as the primary toggle method, with console as fallback. Minor inconsistency. 5. **`enabled?` method on the model instance** (line 215 of the diff, in the controller `update` action): `flag.enabled?` is called as a predicate. ActiveRecord generates boolean predicates (`enabled?`) for boolean columns, so this works. However, the value read here is after `update!`, which is correct. Just noting this is fine. 6. **Stale flag detection query** (lines 368-370): `FeatureFlag.where(enabled: true).where("updated_at < ?", 30.days.ago)` -- this checks `updated_at`, which resets on any update, not just when the flag was enabled. If someone updates the description of an enabled flag, `updated_at` resets and the flag no longer appears stale. Consider adding an `enabled_at` timestamp column, or document this limitation. 7. **Per-business upgrade path** (lines 149-169): The future unique index `[:name, :business_id]` with nullable `business_id` will allow multiple global flags with the same name if `business_id` is NULL, because SQL `UNIQUE` constraints treat NULLs as distinct. This needs a partial unique index: `add_index :feature_flags, :name, unique: true, where: "business_id IS NULL"` plus the composite index for non-null business_id. Worth a note in the "not built now" section so the future implementation does not hit this. ### SOP COMPLIANCE - [x] Branch named after issue: `129-feature-flag-strategy` matches issue #129 - [x] PR body has ## Summary, ## Changes, ## Test Plan: all present and substantive - [x] Related section references project slug: `landscaping-assistant` referenced, closes #129, references #130, #125, #123, #115, #107 - [x] No secrets committed: docs-only, no credentials or API keys - [x] No unnecessary file changes: single file added (`docs/feature-flags.md`), no scope creep - [x] Commit messages: not visible in diff, but PR body is descriptive **PR body note**: The PR body uses `## Review Checklist` and `## Related Notes` instead of `## Related`. This is a minor deviation from the SOP template but the content is complete and functional. ### TEMPLATE CONFORMANCE (Issue #129) I was unable to fetch the issue #129 body directly (no `get_issue` tool available -- `list_issues` returns only titles). Based on the issue title ("Spike: Feature flag strategy (gate features, rollback, gradual rollout)"), I can validate the spike deliverables against the template: **Template requires**: 1. `docs/{topic}.md` created -- YES: `docs/feature-flags.md` (513 lines) 2. Follow-up tickets created -- YES: #130 (implementation ticket) referenced in PR body The spike deliverables match the `template-issue-spike` requirements. Full template conformance of the issue body itself cannot be verified without the issue body text. ### PROCESS OBSERVATIONS - **Documentation quality**: This is an exceptionally thorough architecture doc. The decisions are well-reasoned with explicit alternatives-considered sections. The truth table for flag+role composition, the deployment pipeline interaction diagram, and the rollback scenario analysis are all production-ready documentation. - **Deployment frequency**: No impact -- docs-only PR. The feature-flags system itself is designed to increase deployment confidence by decoupling feature rollout from code deployment. - **Change failure risk**: Low. The main risk is the `db:prepare` vs `db:seed` misunderstanding (nit #2 above), which could cause flag seeding to silently fail on existing databases. - **Cross-doc consistency**: The super admin role introduction (nit #1) creates a documentation gap that should be tracked. Recommend creating a follow-up ticket or adding it to #130's scope. ### VERDICT: APPROVED The architecture doc is technically sound, well-reasoned, and consistent with the existing codebase conventions. The nits above are documentation improvements -- none are blockers. The `db:prepare` seeding nuance (nit #2) and super admin cross-doc gap (nit #1) are the most important items to address, ideally before or during implementation in #130.
ldraney force-pushed 129-feature-flag-strategy from f344b82754
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
to 9c2136b5c1
Some checks are pending
CI / scan_ruby (pull_request) Waiting to run
CI / scan_js (pull_request) Waiting to run
CI / lint (pull_request) Waiting to run
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful
2026-06-06 21:41:18 +00:00
Compare
Fix QA nits: seeding strategy, NULL index, Turbo Stream prereq
Some checks failed
ci/woodpecker/pr/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
CI / scan_ruby (pull_request) Has been cancelled
CI / scan_js (pull_request) Has been cancelled
CI / lint (pull_request) Has been cancelled
a336df9800
- Replace db/seeds.rb with feature_flags:sync rake task — db:prepare
  only seeds on first DB creation, not subsequent deploys
- Fix future per-business unique index to use partial indexes for
  NULL business_id (SQL treats NULLs as distinct)
- Document turbo_stream_from "application" prerequisite for emergency
  broadcast kill-switch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #131 Review

DOMAIN REVIEW

Tech stack: Ruby on Rails 8.1, Hotwire (Turbo Streams/Frames), SolidCache, k3s/CNPG deployment. This is a docs-only PR -- no executable code to lint or test, but the code samples in the doc will be copy-pasted into implementation ticket #130, so correctness matters.

Architecture decisions -- all sound:

  1. DB-backed flags vs ENV vars vs Flipper: Correct reasoning. ENV vars require a pod restart (deploy). Flipper is overkill for global on/off with <10 flags. DB row toggle is instant. Good.

  2. Rails.cache.fetch with 1-min TTL via SolidCache: Appropriate. SolidCache is already configured per docs/multi-database.md. The CACHE_TTL = 1.minute constant is clear and easy to tune. The explicit note about why CurrentAttributes is wrong (resets per-request, no caching benefit) is a nice addition.

  3. pick(:enabled) vs find_by: Correct. pick avoids model instantiation -- returns the raw boolean from a SELECT enabled ... LIMIT 1. Minimal overhead for a hot path.

  4. Rake task feature_flags:sync instead of db/seeds.rb: This was a nit from the previous review and is now correctly addressed. The reasoning is accurate -- db:prepare only runs seeds on first DB creation, so subsequent deploys would never register new flags. The find_or_create_by! pattern is idempotent and preserves existing enabled state.

  5. Two partial unique indexes for NULL uniqueness: Correctly addresses the SQL NULL caveat. WHERE business_id IS NOT NULL and WHERE business_id IS NULL as separate partial indexes is the standard PostgreSQL pattern. This was another previous nit, now resolved.

  6. 404 (not 403) for non-super-admin: Correct security posture. Returning 403 leaks the existence of the route. 404 makes the endpoint invisible.

  7. Turbo Stream emergency broadcast: The prerequisite dependency on turbo_stream_from "application" in the layout is clearly documented, with the Solid Cable ticket (#2) referenced. The "V1 is acceptable without instant propagation" rationale is sound -- flag toggles are rare operational events.

  8. Super admin under Person icon (Platform view), not a new nav tab: Consistent with the 5-slot nav layout documented in docs/user-stories-auth.md. No wasted nav real estate.

  9. initContainer updated to bin/rails db:prepare && bin/rails feature_flags:sync: One-line change in pal-e-deployments. Correct that this is a cross-repo change that needs to happen alongside #130.

Code samples validated:

  • Migration schema is clean Rails 8.1 syntax. null: false on name and enabled, default: false on enabled -- correct.
  • Model validation (validates :name, presence: true, uniqueness: true) matches the DB constraint.
  • enable/disable class methods bust the cache immediately via Rails.cache.delete -- correct.
  • Controller uses before_action for auth, head :not_found -- idiomatic Rails.
  • View uses button_to with method: :patch -- standard Turbo form, no JS needed.
  • Test helper uses ensure for cleanup, which is correct even if the test raises.
  • The before(:each) hook enables all flags and clears cache -- prevents test pollution.

Phase 4 future-proofing: The per-business scoping upgrade path is well thought out. The two-partial-index approach for the nullable FK is the correct PostgreSQL pattern (standard composite unique index treats each NULL as distinct, so [:name, :business_id] would allow duplicate global flags). The fallback lookup (business-specific, then global) is clean.

BLOCKERS

None.

This is a docs-only PR. No executable code, no security surface, no test coverage requirement. All three nits from the previous review have been addressed:

  1. Seeding strategy: rake task instead of db/seeds.rb -- done
  2. NULL uniqueness: two partial indexes -- done
  3. Turbo Stream prerequisite: Solid Cable dependency documented -- done

NITS

  1. README roles table is stale: The README at line 15-20 still shows 4 roles (Client, Crew Member, Crew Lead, Admin) without Super Admin. This PR updates docs/user-stories-auth.md to add Super Admin but does not touch the README. The README is the front door -- new contributors will see "four roles" and then read "five roles" in the auth doc. Consider adding a row for Super Admin in a follow-up or as part of #130. Not blocking because the README roles table is a summary and the super admin role is platform-internal (not a business role), but the discrepancy will confuse readers.

  2. with_feature_enabled helper has a no-op ensure: Lines in the test helper show with_feature_enabled calling FeatureFlag.enable(name) in the ensure block, but the method also starts with FeatureFlag.enable(name). Since the before(:each) hook already enables all flags, the entire with_feature_enabled method is a no-op unless a prior test disabled a flag and the before(:each) hook did not run (which would be a bug). The method exists for symmetry and readability, which is fine, but worth a comment in the implementation noting it is intentionally redundant.

  3. cleanup_after convention is a comment, not a column: The doc mentions "The cleanup_after comment in seeds tracks the deadline" but the convention is only a code comment in the rake task, not an actual cleanup_after column or a structured data field. The stale-flag console query uses updated_at as a proxy instead. This is fine for V1, but the doc could be slightly clearer that the cleanup date is a human convention (comment), not machine-enforced.

SOP COMPLIANCE

  • Branch named after issue: 129-feature-flag-strategy matches spike #129
  • PR body follows template: Summary, Changes, Test Plan, Review Checklist, Related -- all present and substantive
  • Related references project slug: landscaping-assistant referenced, closes #129, links #130, #125, #123, #115, #107
  • No secrets committed: no API keys, passwords, tokens, or .env files in diff
  • No scope creep: docs/feature-flags.md is the spike deliverable, docs/user-stories-auth.md changes are the necessary role updates to integrate super admin
  • Commit messages: clean (visible from branch history)
  • Spike deliverables met: docs/feature-flags.md created (docs/ file), #130 created (follow-up ticket)

PROCESS OBSERVATIONS

  • This spike produced exactly what the template requires: a durable architecture doc and a scoped follow-up ticket (#130). The doc is implementation-ready -- code samples can be lifted directly into the implementation PR.
  • The three-repo interaction diagram is a nice touch -- it makes explicit that feature flags live entirely within the app's runtime and do not require infra changes (except the one-line initContainer update).
  • The rollback scenario section is operationally valuable. The sequence "disable flag, then rollback" is a concrete runbook, not hand-waving.
  • Risk: the initContainer change (bin/rails db:prepare && bin/rails feature_flags:sync) is a cross-repo dependency in pal-e-deployments. Ticket #130 should explicitly list this as a required change or it will be forgotten at merge time.

VERDICT: APPROVED

## PR #131 Review ### DOMAIN REVIEW **Tech stack**: Ruby on Rails 8.1, Hotwire (Turbo Streams/Frames), SolidCache, k3s/CNPG deployment. This is a docs-only PR -- no executable code to lint or test, but the code samples in the doc will be copy-pasted into implementation ticket #130, so correctness matters. **Architecture decisions -- all sound:** 1. **DB-backed flags vs ENV vars vs Flipper**: Correct reasoning. ENV vars require a pod restart (deploy). Flipper is overkill for global on/off with <10 flags. DB row toggle is instant. Good. 2. **`Rails.cache.fetch` with 1-min TTL via SolidCache**: Appropriate. SolidCache is already configured per `docs/multi-database.md`. The `CACHE_TTL = 1.minute` constant is clear and easy to tune. The explicit note about why `CurrentAttributes` is wrong (resets per-request, no caching benefit) is a nice addition. 3. **`pick(:enabled)` vs `find_by`**: Correct. `pick` avoids model instantiation -- returns the raw boolean from a `SELECT enabled ... LIMIT 1`. Minimal overhead for a hot path. 4. **Rake task `feature_flags:sync` instead of `db/seeds.rb`**: This was a nit from the previous review and is now correctly addressed. The reasoning is accurate -- `db:prepare` only runs seeds on first DB creation, so subsequent deploys would never register new flags. The `find_or_create_by!` pattern is idempotent and preserves existing `enabled` state. 5. **Two partial unique indexes for NULL uniqueness**: Correctly addresses the SQL NULL caveat. `WHERE business_id IS NOT NULL` and `WHERE business_id IS NULL` as separate partial indexes is the standard PostgreSQL pattern. This was another previous nit, now resolved. 6. **404 (not 403) for non-super-admin**: Correct security posture. Returning 403 leaks the existence of the route. 404 makes the endpoint invisible. 7. **Turbo Stream emergency broadcast**: The prerequisite dependency on `turbo_stream_from "application"` in the layout is clearly documented, with the Solid Cable ticket (#2) referenced. The "V1 is acceptable without instant propagation" rationale is sound -- flag toggles are rare operational events. 8. **Super admin under Person icon (Platform view), not a new nav tab**: Consistent with the 5-slot nav layout documented in `docs/user-stories-auth.md`. No wasted nav real estate. 9. **`initContainer` updated to `bin/rails db:prepare && bin/rails feature_flags:sync`**: One-line change in pal-e-deployments. Correct that this is a cross-repo change that needs to happen alongside #130. **Code samples validated:** - Migration schema is clean Rails 8.1 syntax. `null: false` on name and enabled, `default: false` on enabled -- correct. - Model validation (`validates :name, presence: true, uniqueness: true`) matches the DB constraint. - `enable`/`disable` class methods bust the cache immediately via `Rails.cache.delete` -- correct. - Controller uses `before_action` for auth, `head :not_found` -- idiomatic Rails. - View uses `button_to` with `method: :patch` -- standard Turbo form, no JS needed. - Test helper uses `ensure` for cleanup, which is correct even if the test raises. - The `before(:each)` hook enables all flags and clears cache -- prevents test pollution. **Phase 4 future-proofing**: The per-business scoping upgrade path is well thought out. The two-partial-index approach for the nullable FK is the correct PostgreSQL pattern (standard composite unique index treats each NULL as distinct, so `[:name, :business_id]` would allow duplicate global flags). The fallback lookup (business-specific, then global) is clean. ### BLOCKERS None. This is a docs-only PR. No executable code, no security surface, no test coverage requirement. All three nits from the previous review have been addressed: 1. Seeding strategy: rake task instead of db/seeds.rb -- done 2. NULL uniqueness: two partial indexes -- done 3. Turbo Stream prerequisite: Solid Cable dependency documented -- done ### NITS 1. **README roles table is stale**: The README at line 15-20 still shows 4 roles (Client, Crew Member, Crew Lead, Admin) without Super Admin. This PR updates `docs/user-stories-auth.md` to add Super Admin but does not touch the README. The README is the front door -- new contributors will see "four roles" and then read "five roles" in the auth doc. Consider adding a row for Super Admin in a follow-up or as part of #130. Not blocking because the README roles table is a summary and the super admin role is platform-internal (not a business role), but the discrepancy will confuse readers. 2. **`with_feature_enabled` helper has a no-op ensure**: Lines in the test helper show `with_feature_enabled` calling `FeatureFlag.enable(name)` in the `ensure` block, but the method also starts with `FeatureFlag.enable(name)`. Since the `before(:each)` hook already enables all flags, the entire `with_feature_enabled` method is a no-op unless a prior test disabled a flag and the `before(:each)` hook did not run (which would be a bug). The method exists for symmetry and readability, which is fine, but worth a comment in the implementation noting it is intentionally redundant. 3. **`cleanup_after` convention is a comment, not a column**: The doc mentions "The `cleanup_after` comment in seeds tracks the deadline" but the convention is only a code comment in the rake task, not an actual `cleanup_after` column or a structured data field. The stale-flag console query uses `updated_at` as a proxy instead. This is fine for V1, but the doc could be slightly clearer that the cleanup date is a human convention (comment), not machine-enforced. ### SOP COMPLIANCE - [x] Branch named after issue: `129-feature-flag-strategy` matches spike #129 - [x] PR body follows template: Summary, Changes, Test Plan, Review Checklist, Related -- all present and substantive - [x] Related references project slug: `landscaping-assistant` referenced, closes #129, links #130, #125, #123, #115, #107 - [x] No secrets committed: no API keys, passwords, tokens, or .env files in diff - [x] No scope creep: `docs/feature-flags.md` is the spike deliverable, `docs/user-stories-auth.md` changes are the necessary role updates to integrate super admin - [x] Commit messages: clean (visible from branch history) - [x] Spike deliverables met: `docs/feature-flags.md` created (docs/ file), #130 created (follow-up ticket) ### PROCESS OBSERVATIONS - This spike produced exactly what the template requires: a durable architecture doc and a scoped follow-up ticket (#130). The doc is implementation-ready -- code samples can be lifted directly into the implementation PR. - The three-repo interaction diagram is a nice touch -- it makes explicit that feature flags live entirely within the app's runtime and do not require infra changes (except the one-line initContainer update). - The rollback scenario section is operationally valuable. The sequence "disable flag, then rollback" is a concrete runbook, not hand-waving. - Risk: the initContainer change (`bin/rails db:prepare && bin/rails feature_flags:sync`) is a cross-repo dependency in pal-e-deployments. Ticket #130 should explicitly list this as a required change or it will be forgotten at merge time. ### VERDICT: APPROVED
ldraney deleted branch 129-feature-flag-strategy 2026-06-06 21:59:43 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/landscaping-assistant!131
No description provided.