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.