Skip to main content

Feature Flags

Feature flags control access to features at the organization level. They are stored as a text array on the organization entity and checked via gates.

Defining Flags

Define available flags in your app's domainActions.ts:

export function availableFeatureFlags() {
return [
{ key: 'example-models', label: 'Example Models', default: true },
] as const
}

export type FeatureFlag = ReturnType<typeof availableFeatureFlags>[number]['key']

export function getDefaultFeatureFlags(): string[] {
return availableFeatureFlags().filter(f => f.default).map(f => f.key)
}

Each flag has:

  • key — unique identifier, used in gates and stored in the database
  • label — human-readable name for the superadmin UI
  • default — whether the flag is enabled for newly created organizations

The FeatureFlag type is a string literal union derived from the keys, giving you type safety when referencing flags.

featureFlagGate

The @repo/auth package exports featureFlagGate, a gate factory that checks whether the session's organization has a specific flag enabled:

import { featureFlagGate } from '@repo/auth'

const gate = featureFlagGate('example-models')

Returns createGateResult(false, 403) when the flag is not present in session.entity.organization.featureFlags.

Type-safe Wrapper

Apps define a typed wrapper around featureFlagGate that restricts the argument to valid FeatureFlag keys:

// authHelpers.ts
import type { FeatureFlag } from '$lib/library/domainActions'
import { featureFlagGate } from '@repo/auth'

export function featureFlag(flag: FeatureFlag) {
return featureFlagGate(flag)
}

Use this wrapper instead of calling featureFlagGate directly to catch typos at compile time.

Server-side Usage

Combine feature flag gates with permission gates in validateSession:

import { createPermissionGate, featureFlag, validateSession } from '$lib/library/authHelpers.server'

export const getExampleModelsQuery = query(schema, async (data) => {
await validateSession([
featureFlag('example-models'),
createPermissionGate('exampleModels', ['list']),
])

// ... fetch data
})

Both gates must pass. The feature flag gate runs first — if the org doesn't have the flag, the user gets a 403 regardless of their permissions.

Client-side Usage

Use hasAccess from @repo/auth/client to conditionally show navigation items or UI sections:

<script lang="ts">
import { featureFlag } from '$lib/library/authHelpers'
import { hasAccess } from '@repo/auth/client'

const [hasExampleModels] = await Promise.all([
hasAccess([featureFlag('example-models')]),
])
</script>

Then conditionally render menu items:

const menu = {
main: [
...(hasExampleModels
? [{ title: 'Example Models', url: '/example-models', icon: FlaskConical }]
: []),
],
}

Client-side checks are for UI only — server-side validateSession is the real enforcement.

Superadmin Management

Superadmins manage feature flags per organization at /superadmin/organizations/[id]. The page displays all availableFeatureFlags() as toggles with a "(default)" badge on flags that are enabled by default.

Toggling a flag calls updateOrganizationFeatureFlagsCommand, which requires both isSuperadminGate and notImpersonatingGate.

Adding a New Feature Flag

  1. Add the flag definition to availableFeatureFlags() in domainActions.ts
  2. Use featureFlag('your-flag') in validateSession for server-side enforcement
  3. Use hasAccess([featureFlag('your-flag')]) in layouts/pages for client-side gating
  4. The superadmin UI automatically picks up the new flag — no UI changes needed

Storage

Feature flags are stored as a text[] column on the globalOrganizationsTable. New organizations receive getDefaultFeatureFlags() during creation.