Feature-Flags — Entwicklerreferenz
Wie das Feature-Flag-System von Owlat funktioniert: Single Source of Truth, Auflösung von Abhängigkeiten, Zuordnung zu Docker-Profilen und wie Sie ein neues Flag hinzufügen.
Feature-Flags — Entwicklerreferenz
Die betreiberseitige Übersicht finden Sie unter Feature-Flags in der Produktanleitung. Diese Seite richtet sich an Entwickler, die Flags hinzufügen, entfernen oder erweitern.
Single Source of Truth
Jede umschaltbare Oberfläche ist in packages/shared/src/featureFlags.ts deklariert. Das Setup-CLI, die Admin-Oberfläche, das Convex-Backend und die Nuxt-Middleware importieren alle aus dieser einen Datei — es gibt keine zweite Registry, die synchron gehalten werden müsste.
export const FEATURE_FLAGS: Record<FeatureFlagKey, FeatureFlagDefinition> = {
postbox: {
key: 'postbox',
category: 'receiving',
label: 'Personal mail (Postbox)',
description: 'Per-user mailboxes with webmail UI, IMAP/SMTP for native clients, and MX-based delivery (Gmail-equivalent).',
default: false,
dockerProfiles: ['personal-mail'],
},
// … 30 more flags (31 total in FEATURE_FLAGS)
};
Jede Definition kann Folgendes deklarieren:
| Feld | Zweck |
|---|---|
category | Gruppiert das Flag in der Admin-Oberfläche (sending / receiving / ai / integrations / security / deliverability / hosted) |
default | Anfangswert für frische Installationen |
requires | Andere Flag-Schlüssel, die aktiviert sein müssen, damit dieses Flag aktiv sein kann |
cascadesOff | Flags, die zwangsweise deaktiviert werden, wenn dieses deaktiviert wird (wird außerdem automatisch aus requires abgeleitet) |
requiredEnvVars | Umgebungsvariablen, die Assistent/Oberfläche vor der Aktivierung erfassen müssen |
dockerProfiles | Compose-Profile, die aktiviert werden, wenn dieses Flag aktiv ist |
hostedOnly | Im Self-Host-Assistenten ausgeblendet (Control-Plane-Flags) |
Laufzeit-Helfer
import {
FEATURE_FLAGS,
resolveFlags,
isFlagEnabled,
applyToggle,
getActiveProfiles,
getRequiredEnvVars,
getFlagsByCategory,
FEATURE_PACKS,
applyPackToggle,
isPackEnabled,
} from '@owlat/shared/featureFlags';
| Helfer | Einsatz |
|---|---|
resolveFlags(stored) | Aktuellen Zustand lesen — wendet requires-Kaskaden an und liefert den effektiven Zustand |
isFlagEnabled(stored, key) | Bequemer Zugriff auf resolveFlags(...)[key] |
applyToggle(stored, key, value) | Benutzer hat einen Schalter betätigt — liefert { next, cascaded }, damit die Oberfläche zeigen kann, was sich sonst noch geändert hat |
getActiveProfiles(stored) | Die zu aktivierenden Docker-Compose-Profile berechnen |
getRequiredEnvVars(stored) | Welche Umgebungsvariablen bei der aktiven Flag-Menge abgefragt werden müssen |
getFlagsByCategory() | Die Admin-Oberfläche nach Kategorien gruppiert rendern |
applyPackToggle(stored, packKey, value) | Alle Flags eines Packs auf einmal umschalten (Kaskaden greifen weiterhin) |
Die Auflösung erfolgt iterativ bis zum Fixpunkt mit einer Sicherheitsgrenze von 10 Iterationen — lange Abhängigkeitsketten (inbox.codeTasks → ai.agent → ai + inbox) werden in einem einzigen Aufruf aufgelöst.
Speicherung & Durchsetzung
┌─────────────────────────────────────────────────────┐
│ packages/shared/src/featureFlags.ts (registry) │
│ resolveFlags, applyToggle, getActiveProfiles, … │
└────┬───────────────┬───────────────┬────────────────┘
│ │ │
┌───────────▼────────┐ ┌──▼─────────────────┐ ┌─▼─────────────────────┐
│ apps/setup-cli │ │ apps/api │ │ apps/web │
│ commands/feature │ │ organizations/ │ │ middleware/ │
│ commands/pack │ │ featureFlags.ts │ │ feature.global.ts │
│ lib/override │ │ lib/featureFlags.ts│ │ pages w/ requires- │
│ lib/flagState │ │ + queries │ │ Feature: '<flag>' │
└───────┬────────────┘ └────────┬───────────┘ └───┬───────────────────┘
│ │ │
writes docker-compose │ route gate →
.override.yml + .owlat- │ /dashboard?disabled=<flag>
flags.json │
source of truth:
Convex table
instanceSettings.featureFlags
- Convex ist zur Laufzeit die Source of Truth.
instanceSettings.featureFlagsist ein einzelnes Dokument. - Das Setup-CLI spiegelt den Zustand in
.owlat-flags.jsonneben der Compose-Datei, damit der Betreiber Flags ändern kann, bevor der Stack läuft. Beim Start gleicht Convex die Datei mit der Datenbank ab. - Docker-Compose-Profile werden indirekt aktiviert:
owlat-setupgeneriertdocker-compose.override.ymlausgetActiveProfiles(...)neu. Die Basis-Compose-Datei deklariertprofiles: [<name>]an jedem gegateten Service. - Page Guards: Seiten deklarieren in
definePageMetaeinrequiresFeature(ein einzelner Schlüssel oder ein Array — alle müssen aktiv sein) und optional einrequiresAnyFeature(eine ODER-Gruppe, mindestens eines aktiv). Die dedizierte globale Middlewareapps/web/app/middleware/feature.global.tsliest beides und leitet auf/dashboard?disabled=<flag>um, wenn ein erforderliches Flag aus ist.requiresAnyFeatureexistiert für Oberflächen, die über mehr als eine Funktion erreichbar sind — z. B. die Postbox-Oberfläche unter dem gehostetenpostboxodermail.external. - Serverseitige Guards: Öffentliche Convex-Funktionen in gegateten Modulen rufen früh
await assertFeatureEnabled(ctx, '<flag>')auf (apps/api/convex/lib/featureFlags.ts), damit ein veralteter Client die Gates der Oberfläche nicht umgehen kann. Der Helfer liestinstanceSettings.featureFlags, löst Abhängigkeiten über das gemeinsameresolveFlagsauf und wirft im deaktivierten Fall einenforbidden-Fehler.
Feature-Packs
Packs sind reiner Oberflächen-Zucker — sie führen keinen neuen Zustand ein, sondern lesen und schreiben denselben FeatureFlagState über applyPackToggle, das pro Flag applyToggle aufruft, damit sich Kaskaden korrekt verhalten.
export const FEATURE_PACKS: Record<FeaturePackKey, FeaturePack> = {
marketing: { flags: ['campaigns', 'automations', 'transactional'], /* … */ },
emailClient: { flags: ['inbox', 'chat', 'postbox'], /* … */ },
ai: { flags: ['ai', 'ai.agent', 'ai.autonomy', 'ai.knowledge', 'ai.assistant', 'ai.visualizations'] },
};
Ein neues Flag hinzufügen
- Deklarieren Sie es in
packages/shared/src/featureFlags.ts— ergänzen Sie die UnionFeatureFlagKeyund die MapFEATURE_FLAGS. - Entscheiden Sie über Abhängigkeiten. Verwenden Sie
requiresfür harte Voraussetzungen; der Resolver und der Cascade-Off-Helfer erledigen den Rest automatisch. - (Optional) Deklarieren Sie ein Docker-Profil, falls das Flag einen zusätzlichen Service benötigt. Fügen Sie
profiles: [<name>]beim entsprechenden Service ininfra/templates/docker-compose.vps.ymlund in derdocker-compose.ymlim Wurzelverzeichnis hinzu. - (Optional) Deklarieren Sie Umgebungsvariablen in
requiredEnvVars. Der Assistent fragt sie ab; die Admin-Oberfläche verhindert das Einschalten, bis sie gesetzt sind. - Gaten Sie die Oberfläche. Fügen Sie
requiresFeature: '<key>'zumdefinePageMetajeder Seite hinzu, die zur Funktion gehört, und rendern Sie Sidebar-Einträge bedingt. - Gaten Sie den Server. Rufen Sie am Anfang jeder öffentlichen Convex-Funktion hinter dem Flag (nach der Auth-Prüfung)
await assertFeatureEnabled(ctx, '<key>')ausapps/api/convex/lib/featureFlags.tsauf — die Funktion liest die gespeicherten Flags, löst Abhängigkeiten auf und wirft selbstständig einenforbidden-Fehler. - Aktualisieren Sie die Dokumentation: Nehmen Sie das Flag in die Tabelle unter Feature-Flags in der Produktanleitung auf und referenzieren Sie es (falls für Benutzer sichtbar) aus der Feature-Tabelle in der README.
Wenn Sie eine neue Kategorie einführen, ergänzen Sie sie außerdem in der Union FeatureCategory sowie mit einem Label im Kategorie-Sortierer der Admin-Oberfläche.
Ein Flag entfernen
- Setzen Sie es für ein Release auf
default: false, um Betreibern Zeit für die Migration zu geben. - Entfernen Sie die Page-Meta-Gates und bedingten Renderings und lassen Sie den zugrunde liegenden Codepfad entweder dauerhaft aktiv (wenn Sie ihn behalten) oder löschen Sie ihn vollständig (wenn Sie die Funktion ausmustern).
- Entfernen Sie den Eintrag aus
FEATURE_FLAGSund der UnionFeatureFlagKey. Der Resolver ignoriert unbekannte gespeicherte Schlüssel anstandslos, das Entfernen ist also gefahrlos — alte.owlat-flags.json-Dateien gehen dabei nicht kaputt. - Entfernen Sie das Docker-Profil aus den Compose-Dateien.
Nur gehostete Flags
billing.stripe, multiTenancy und tier.autoProvision sind mit hostedOnly: true markiert. getDefaultFlags() und getFlagsByCategory() überspringen sie direkt, sofern nicht { hosted: true } übergeben wird, sodass das Self-Host-Setup-CLI und die Admin-Oberfläche sie nie zu Gesicht bekommen. resolveFlags() filtert sie nicht selbst — es merged lediglich über die (gefilterte) Standard-Baseline, sodass ein hostedOnly-Schlüssel, der explizit in stored vorhanden ist, dennoch durchaufgelöst würde. Sie werden extern von der gehosteten Control Plane (in einem separaten privaten Repository) gesetzt und von apps/api für die Sichtbarkeit der Abrechnungsoberfläche gelesen.
Tests
Unit-Tests für den Resolver liegen in packages/shared/src/__tests__/featureFlags.test.ts. Wenn Sie ein Flag mit nicht trivialen Abhängigkeiten hinzufügen, ergänzen Sie dort einen Fall, der die Kaskade abdeckt. Die eigenen Tests des Setup-CLI liegen in apps/setup-cli/src/lib/__tests__/ (passwordHash.test.ts, convexDeploy.test.ts).