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 databaselabel— human-readable name for the superadmin UIdefault— 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
- Add the flag definition to
availableFeatureFlags()indomainActions.ts - Use
featureFlag('your-flag')invalidateSessionfor server-side enforcement - Use
hasAccess([featureFlag('your-flag')])in layouts/pages for client-side gating - 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.