Track beta cohort on Secrets Manager enrollments
What does this MR do and why?
Adds a beta boolean to secrets_manager_namespace_enrollments so we can tell the free beta cohort
apart from namespaces that enroll once Limited Availability ships on GitLab.com.
NamespaceEnrollmentService#enroll sets it to
!Feature.enabled?(:secrets_manager_paid_experience, namespace). Namespaces that enroll before the
paid experience reaches them are beta. Everything after is not.
This is needed because Limited Availability lets both cohorts exist at the same time. The beta cohort keeps free access until a cutoff date, while new signups go to trial or paid. Without a marker on the row we cannot tell which is which.
Why a column and not created_at
The cheaper looking option is to skip the schema change and ask created_at < <LA date>. That does
not work here:
secrets_manager_paid_experienceis actor scoped and rolls out per namespace. There is no single instant separating the two cohorts, so two rows created in the same minute can sit on different sides of the line.- On GitLab.com a release is not an instant. Deploys roll out in stages, and the flag flip is a separate event from the deploy anyway.
- We ship to GitLab.com continuously from
master, so a change tagged for a milestone can be live well before that milestone is cut. The milestone on this migration says nothing about when the behavior reached users.
Any cutoff date would be a hardcoded constant that is wrong at the edges, and the rows landing in that window are exactly the ambiguous ones we care about. The flag is the real switch, not the release, so the only reliable way to record cohort is at write time from the flag itself.
Why a column and not a separate table
Moving records to a beta_enrollments table (the other option discussed on the work item) needs
multiple deploy steps to be safe: backfill, dual read and write, then cutover. An additive column is
a single step. Old code ignores it and keeps working.
Why the default is true
Migrations run before the new application code is deployed. During that window every enrollment
created is a beta enrollment by definition, so rows written by old code are still marked correctly.
Once the new code is live it passes beta explicitly.
Not in scope
- Trial vs paid is not tracked here. That belongs to the fulfillment domain, which already owns it. Adding it would create a second source of truth for billing state.
- No GraphQL field yet. The banner UX is still in design, so the field lands with the UI work.
- Namespaces that unenroll during beta leave no trace, since unenroll deletes the row. Accepted as out of scope on the work item thread.
Follow-up
Enforcement lands separately, gated by a new end_secrets_manager_beta_program flag (not yet implemented). That flag is
deliberately phrased in the negation so it ships default off, gets enabled on GitLab.com at the
cutoff date, and is removed in the milestone that ends beta for self-managed. Removing a flag makes
the enabled branch permanent, so the state we want to keep forever has to be the enabled one.
Note these are two different questions and need two different flags:
| Question | When | Source |
|---|---|---|
| Is this enrollment part of the beta cohort? | Once, at write time | secrets_manager_paid_experience (this MR) |
| Does the beta cohort still get free access? | Every access check | end_secrets_manager_beta_program (follow-up) |
They disagree during the overlap window. Once LA is live but before the cutoff date, a new signup is not beta while the beta program is still running. A single flag cannot answer both.
Database
Affected database: main (gitlab_main_org schema). Verified up and down on main, ci, and sec
locally. The table is small and low traffic, so a regular migration with a default is safe in one
step.
Migration up:
main: == 20260807153843 AddBetaToSecretsManagerNamespaceEnrollments: migrating ======
main: -- add_column(:secrets_manager_namespace_enrollments, :beta, :boolean, {:default=>true, :null=>false})
main: -> 0.0323s
main: == 20260807153843 AddBetaToSecretsManagerNamespaceEnrollments: migrated (0.0374s)Migration down:
main: == 20260807153843 AddBetaToSecretsManagerNamespaceEnrollments: reverting ======
main: == 20260807153843 AddBetaToSecretsManagerNamespaceEnrollments: reverted (0.0700s)Resulting change to db/structure.sql:
CREATE TABLE secrets_manager_namespace_enrollments (
id bigint NOT NULL,
namespace_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL
+ updated_at timestamp with time zone NOT NULL,
+ beta boolean DEFAULT true NOT NULL
);Backward compatibility and rollback
Purely additive. No existing read path looks at the column, so old and new code run side by side during the deploy. Rolling back drops the column and restores the previous behavior with no data fixup needed.
How to set up and validate locally
Namespace enrollment is GitLab.com only, so the GDK needs to simulate SaaS.
-
Run the migration:
bundle exec rails db:migrate -
Start the console with SaaS simulation:
GITLAB_SIMULATE_SAAS=1 bundle exec rails console -
Enroll with the paid experience off and confirm the row is marked beta:
group = Group.find_by_full_path('flightjs') user = group.owners.first&.user || User.admins.first Feature.disable(:secrets_manager_paid_experience) Feature.enable(:secrets_manager_namespace_enrollment) SecretsManagement::NamespaceEnrollmentService.new(group, current_user: user) .enroll.payload[:enrollment].beta # => true -
Unenroll, turn the paid experience on, and confirm the next enrollment is not beta:
SecretsManagement::NamespaceEnrollmentService.new(group, current_user: user).unenroll Feature.enable(:secrets_manager_paid_experience) SecretsManagement::NamespaceEnrollmentService.new(group, current_user: user) .enroll.payload[:enrollment].beta # => false
The group also needs the native_secrets_management licensed feature for enrollment to be allowed.
Specs:
bundle exec rspec ee/spec/models/secrets_management/namespace_enrollment_spec.rb \
ee/spec/services/secrets_management/namespace_enrollment_service_spec.rbReferences
- Work item: https://gitlab.com/groups/gitlab-org/-/work_items/22209
- Design discussion for this approach: https://gitlab.com/groups/gitlab-org/-/work_items/22209#note_3653855792
- Earlier thread comparing the column against a separate table: https://gitlab.com/groups/gitlab-org/-/work_items/22209#note_3424816474
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.