Docs / Admin panel / Feature flags & settings
Feature flags & settings
The /admin/settings page has two tabs: feature flags (per-team and global) and a small set of global app settings. Both are admin-managed configuration stored in the database, wired into the panel's UI but, in v1, not yet read by any runtime code path, they are the config surface you build on top of, not a live gate out of the box.
Feature flags
Flags live in the feature_flags table (lib/db/schema/feature-flags.ts), managed through lib/dal/admin-flags.ts. A flag has a key (mono, [a-z0-9-]), a display name, a description, an enabled boolean, and a scope.
The override model is one table doing double duty. A row with team_id IS NULL is the global default for its key; a row with a team_id overrides that default for one team.
A single global row per key is enforced in app code, not the DB
The unique index is on (key, team_id), and Postgres allows multiple NULLs in a unique index, so the database would happily accept two global rows for the same key. getGlobalFlagByKey and the create path enforce "one global row per key" in code, keep new flag logic going through the DAL so that invariant holds.
scope only means anything on the global row and controls whether the UI offers per-team overrides at all. A global-scoped flag is deliberately override-proof (think a site-wide kill switch); setFlagOverride returns null rather than creating an override for one, so you never get a silent override row that no resolver reads. Deleting a flag (deleteFlagAndOverrides) removes the global row and every per-team override sharing its key in one action.
Wiring a flag into behavior is the buyer's step: read getGlobalFlagByKey(key) (and, for per-team flags, the team's override row) wherever you want to gate a feature, then flip it from the admin panel.
Global settings
The Global settings tab is a single row in app_settings (lib/db/schema/app-settings.ts), read and upserted through lib/dal/admin-settings.ts. Typed columns, not a key/value store, since the set is small and known: maintenanceMode, pauseSignups, defaultTeamRole, pauseOutgoingEmail, and allowTeamCreation. The migration seeds the row, so getAppSettings effectively always finds it.
Like flags, these toggles are stored and editable but not yet enforced anywhere, they are the hooks for behavior you add. When you wire them up, read them where the relevant action happens: check pauseSignups in the sign-up route, pauseOutgoingEmail in lib/email.ts, allowTeamCreation in POST /api/teams, and so on.
Note defaultTeamRole is a per-team role (member by default), never owner, owner is only ever assigned by team-creation logic itself, never handed to a sign-up.