Organization
The Organization initiative focuses on reaching feature parity between GitLab.com and GitLab Self-Managed.
Guidance for feature teams
Use this section to understand what to consider before building a feature at the organization level.
The target milestone for launching Organizations as Beta is 19.4 (2026-09-11).
Previously, a feature implemented at the instance level for GitLab Self-Managed had to be re-implemented for top-level groups on GitLab.com. Organizations remove this duplication. The new default is to build features at the organization level, which serves both. For example, the Artifact Registry uses organizations as its anchor point.
Not every feature needs organization-level scope, and because Organizations has not yet launched as Beta, there are key considerations to be aware of. Read the following sections for details.
Contact the team on Slack (#g_organizations) to discuss your use case before building.
Determine if your feature needs organization-level scope
Not every feature belongs at the organization level.
Most features should continue to be anchored to the group, project, or user level. This mirrors the old paradigm where most features did not target the instance or TLG level.
Features that span multiple groups inside an organization should be scoped at the group level, but with cross-group navigation. It is not a sufficient justification to invent an organization-level version.
Only build a feature at the organization level when users have a clear need for organization-level governance or configuration.
Consult the Organizations Charter
(https://docs.google.com/document/d/1ldPftCifCDkdw3_3JKOnFjdwNHGIgbHIbW8HEc92i1Y/edit, internal access required)
for more information.
Plan roles specific to your organization-level feature
Organization-level roles are separate from group and project roles. When designing your feature:
- Define what actions each organization role (Owner, User) can perform.
- Do not assume group-level roles map directly to organization-level roles.
- Consider whether your feature requires a new organization-level permission, or whether an existing role is sufficient.
For more information, see the organization user documentation.
Require the top-level group to transfer to its own organization (GitLab.com)
Features that depend on organization context require the TLG to be inside its own organization. This is because the default organization on GitLab.com currently contains TLGs where the TLG owners are not Organization Owners.
Do not enforce this with a check such as organization.default?. GitLab Self-Managed and GitLab
Dedicated legitimately run inside the default organization, so that check would also block them.
Instead, enforce this through authorization, using the organization-level permissions described in
Plan roles specific to your organization-level feature.
Gate the feature behind a permission that only an Organization Owner can grant, for example through
the organization_owner and organization_user conditions in Organizations::OrganizationPolicy.
Consider organization-based billing limitations
Organization-based billing is not yet available.
Do not build features that depend on billing or subscription entitlements at the organization level.
This is because billing and subscription entitlements are currently scoped to the top-level group on GitLab.com, not to the organization. Most paid customers only have one paid TLG on GitLab.com, so this rarely comes up. If you create another TLG, that new TLG does not inherit the paid entitlements of your existing TLG.
Users are warned of this before they attempt to create another TLG on GitLab.com.
Available and planned support for implementing organizations
The Organizations team is implementing changes which will automatically include support for:
- Application level Organization Isolation: There will be an ActiveRecord extension that will take care of Organization Scoping. This is provisionally planned for availability and usage in early FY27-Q2.
- Sidekiq: there is no need to pass
organization_idto Sidekiq worker parameters: Sidekiq workers will inherit the Current Organization from the scheduling context - Events / Logging: similar to User, Project, or Namespace, Organization will be included
- Routing: Enabling / disabling organization based URLs (
/o/<organization>prefix) will be available. - Organization availability in tests
Teams do not need to implement these, unless there are specific reasons.
Releasing organization features
Organization features ship behind an organization flag that moves through a fixed ladder of stages, from Experimental to generally available (GA).
A feature’s audience only ever grows as its organization flag advances to a later stage, so an earlier stage’s audience is never dropped.
For the engineering guide on gating a feature, registering an organization flag, and advancing it through the stages, see Organizations release process. For the organization flags currently in the rollout process and their stage, see Organizations platform release status.
Database table design
See the sharding guidelines.
Using Current.organization
Ensure that Current.organization is set correctly at the request layer.
For the cases where this is not set automatically, follow the steps below.
Once Current.organization is set, the ActiveRecord extension
(gitlab-database-data_isolation) will use this
context to conditionally scope queries to that organization.
Where Current.organization is available
Current.organization is set automatically in the following contexts:
- Controllers:
ApplicationControllerincludes abefore_action :set_current_organizationthat runs for every request. - GraphQL:
GraphqlControllerinherits fromApplicationController, so the samebefore_actionapplies automatically. - Grape API: a global
before_validationhook inlib/api/api.rbruns for every endpoint. The hook resolves the organization from theX-GitLab-Organization-IDheader, then from the organization of the authenticated user, and falls back to the default organization. - Sidekiq: set from the organization context captured when the job is enqueued.
You must set Current.organization yourself in these cases:
Grape API classes that opt out of the global hook with
skip_global_organization_setup!. The global hook derives the organization from standard API authentication, such as personal access tokens. If your endpoint uses a custom authentication mechanism (for example, deploy tokens), the hook cannot resolve the correct organization. Opt out and derive the organization from the authenticated entity instead:class MyAPI < ::API::Base skip_global_organization_setup! before do Current.organization = some_custom_method end endCode that runs outside a request or Sidekiq context, such as Rake tasks and the Rails console.
Passing organization context
If there is application logic that needs the Current.organization, it should be passed from the request layer:
# In controllers
def create
@group = Groups::CreateService.new(
current_user,
group_params.with_defaults(organization_id: Current.organization.id)
).execute
endScoping queries to organizations
An ActiveRecord extension (gitlab-database-data_isolation) scopes queries to the
current organization, dependent on the isolation state of the organization.
For more information, see Organization data isolation.
Organization routing
Organization-scoped routes use the /o/:organization_path/ pattern (for example, /o/my-org/projects).
These URL helpers exist so a feature’s views and links work globally and inside an organization, without maintaining two versions.
Use regular, unscoped Rails URL helpers like projects_path and project_issues_path(@project).
The routing layer decides at call time whether to nest the generated URL under an organization path, or return the plain global URL.
projects_path # /projects, or /o/my-org/projects if the request resolves to organization my-org
project_issues_path(@project) # /namespace/project/-/issues, or /o/my-org/namespace/project/-/issuesRoute pairing
The implementation lives in Routing::OrganizationsHelper::MappedHelpers.
When routes load, it:
- Scans all routes to find organization-scoped ones, those whose path includes
/o/:organization_path. - Pairs each organization-scoped route with a global route of the same name, by stripping the
organization_ororganizations_prefix from the organization route’s name. For example,organization_projects_pathpairs withprojects_path. Only names present on both sides are paired. - Overrides each paired global URL helper, both the
_pathand_urlvariant, so it can nest the generated URL or fall back to the plain global route. - Preserves the original
root_url,root_path,group_canonical_url, andgroup_canonical_pathhelpers asunscoped_root_url,unscoped_root_path,unscoped_group_canonical_url, andunscoped_group_canonical_path.
Route pairing relies on name convention alone, not on matching controller and action. As a result, an instance administrator whose home organization is isolated could get instance admin links incorrectly nested under their organization. The plan is to keep instance administrators from being members of isolated organizations, avoiding this case entirely.
Current.data_context
Current.data_context identifies the data-isolation boundary for the current request: the single organization or user whose data the request is confined to, if any.
It is set once per request by CurrentDataContext#set_data_context (app/controllers/concerns/current_data_context.rb).
It wraps a Gitlab::Current::DataContext (lib/gitlab/current/data_context.rb), built from organization: Current.organization_resolver.from_request and user: current_user.
Current.organization is always set, falling back to the default organization when none applies.
Current.data_context only resolves to an organization when that organization is actually isolated.
DataContext#typereturns:organizationwhen an isolated organization applies,:userwhen a user is present but no organization is isolated, or:nilotherwise.- A non-isolated organization is never treated as a boundary (
Organization#isolated?must be true). - If the current user’s home organization is isolated, that organization is always the boundary, even when the request names a different organization.
- The request’s own organization (the one it names, for example through the URL) only becomes the boundary when the user side does not already claim one: no current user exists, or the current user’s home organization is not isolated.
For more information, see the organization contexts design document.
Current.organization_resolver
Current.organization_resolver is a Gitlab::Current::Organization (lib/gitlab/current/organization.rb), set once per request by CurrentOrganization#set_current_organization (app/controllers/concerns/current_organization.rb).
It exposes:
from_organization_params: the organization named by the/o/:organization_pathsegment of the current request’s URL, if any.from_request:from_organization_params, or the organization named by a group or project namespace in the URL parameters, or the organization named by theX-GitLab-Organization-IDheader.organization:from_request, or the organization of the current user, or the default organization.
Deciding which organization to nest under
For each paired helper call, the organization path to nest under is decided in this order, stopping at the first step that applies:
Current.data_context, if it resolves to organization context (itstypeis:organization). Use that organization’s path. This step is checked, and returns, before either of the following steps run. It overrides even an explicit per-call override, because a user whose home organization is isolated, or an anchor organization that is itself isolated, has no existence outside that organization.- An explicit
organization_path:keyword argument passed to the helper call, only reached when step 1 does not apply. Because this checks for the key’s presence rather than its truthiness, passingorganization_path: nilforces the plain global path, even though step 3 would otherwise nest it. - The organization named by the current request’s own URL, checked through
Current.organization_resolver.from_organization_params. This only applies if the request’s path itself already contains an/o/:organization_pathsegment.
If none of these steps applies, the plain global route is used.
Explicit organization helpers
Use explicit organization helpers outside the request layer, such as in services, workers, or Rake tasks, where the automatic resolution above does not apply. Also use them when a caller needs an organization other than the one automatic resolution would pick:
organization_projects_path(organization_path: 'my-org') # /o/my-org/projects
organization_project_issues_path(@project, organization_path: 'my-org') # /o/my-org/namespace/project/-/issues
projects_path(organization_path: nil) # /projects, even inside an organization-scoped requestRoutes not yet organization-scoped
Some routes are not currently available under the organization scope:
- Devise OmniAuth callbacks - Devise does not support scoping OmniAuth callbacks under a dynamic segment, so these remain at the global level
- API routes - API endpoints are not yet organization-scoped
Testing organization isolation
Enable the following feature flags to test organizations:
ui_for_organizationsorg_stage_experimental, which enables the Experimental-stage organization flags, such asorg_creation
For more information on organization flags, see the organizations release process.
When making features organization-aware, pay special attention to areas where cross-organization data leakage could occur. Examples include:
- Group and project member invites
- User mentions in issues, merge requests, or comments
- User search and autocomplete results
- Issue, merge request, milestone, and label references across organizations
- Finder classes scoping results to the current organization
A helpful convention for manual testing in your development environment is to create an organization with an obvious name and prefix all its associated data. This makes it easy to visually confirm whether data from other organizations has accidentally been exposed.
Create an Organization named Secret Tanuki and prefix all its associated data with this name:
- Organization:
Secret Tanuki - Users:
Secret Tanuki User Bob,Secret Tanuki User Alice - Projects:
Secret Tanuki Project X,Secret Tanuki Project Y - Issues:
Secret Tanuki Issue #42,Secret Tanuki Issue #99 - Groups:
Secret Tanuki Group - Merge Requests:
Secret Tanuki MR: Add feature
When testing for data leaks, search your UI or API responses for Secret Tanuki. If you find it where it shouldn’t be,
you’ve discovered a cross-organization data leak. This is particularly useful when:
- Testing search and autocomplete features
- Verifying member invitations don’t leak across organizations
- Checking that mentions and references are properly scoped
- Reviewing API responses for unintended data exposure
Automated testing
For automated testing strategies, see Testing with Organizations.
Frontend guidelines
REST API and GraphQL requests
Providing the current organization context to REST API and GraphQL requests does not require any additional arguments. Behind the scenes the current organization is passed via the X-GitLab-Organization-ID header in axios_utils.js#L15 and graphql.js#L183.
URLs
Do not hardcode or construct URLs on the frontend as they will not support organization routing. See URLs in GitLab for guidelines on how to generate URLs on the frontend.
Accessing the current organization
The current organization context is available on the frontend via window.gon.current_organization. Behind the scenes this is exposed to the frontend in gon_helper.rb#L69.