Skip to content

Implement feature flag system#1823

Open
paustint wants to merge 1 commit into
mainfrom
feat/add-feature-flags
Open

Implement feature flag system#1823
paustint wants to merge 1 commit into
mainfrom
feat/add-feature-flags

Conversation

@paustint

Copy link
Copy Markdown
Contributor

Introduce a feature flag system that allows for client-side verification and database support for user and team-specific overrides. This system enhances feature management by enabling dynamic control over feature availability based on user entitlements.

Copilot AI review requested due to automatic review settings June 23, 2026 03:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a lightweight, code-defined feature flag registry with database-backed per-user/per-team overrides, plus server-side signing and browser-side signature verification so the UI can consume “trusted” resolved flags.

Changes:

  • Add FeatureFlagOverride Prisma model + migration to persist user/team-scoped flag overrides.
  • Introduce shared @jetstream/types flag registry + deterministic serialization for signing/verification.
  • Resolve + sign flags in the API /api/me profile path, verify once on the client, and consume flags via new Jotai state/hook (with an example UI gate in AppHome).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
prisma/schema.prisma Adds FeatureFlagOverride model and relations on User/Team.
prisma/migrations/20260623031854_add_feature_flag_override/migration.sql Creates the feature_flag_override table and indexes/FKs.
libs/types/src/lib/types.ts Extends UserProfileUi with featureFlags + featureFlagsSignature.
libs/types/src/lib/feature-flags.ts Adds shared feature flag registry + stable signing serialization.
libs/types/src/index.ts Re-exports feature flag utilities/types.
libs/shared/ui-utils/src/lib/shared-feature-flag-utils.ts Adds browser-side signature verification and extraction of trusted flags.
libs/shared/ui-utils/src/index.ts Exports the new UI feature flag helper.
libs/shared/ui-core/src/app/AppHome/AppHome.tsx Adds feature-flag gating for home cards.
libs/shared/ui-app-state/src/lib/ui-app-state.ts Verifies flags once during profile fetch; adds featureFlagsState + useFeatureFlag.
libs/api-config/src/lib/env-config.ts Adds JETSTREAM_FEATURE_FLAG_PRIVATE_KEY env support.
apps/api/src/app/services/feature-flag-signing.service.ts Implements ECDSA P-256 signing (ieee-p1363) for resolved flags.
apps/api/src/app/services/tests/feature-flag-signing.service.spec.ts Tests signature validity and tamper detection.
apps/api/src/app/db/user.db.ts Resolves + signs flags when building the user-facing profile.
apps/api/src/app/db/feature-flags.db.ts Adds DB-backed flag resolution + optional authoritative server-side gate helper.
apps/api/src/app/db/tests/feature-flags.db.spec.ts Tests resolution behavior, scoping, and gating.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +23
function base64ToBytes(value: string): Uint8Array<ArrayBuffer> {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
const binary = atob(normalized);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
logger.warn('[FEATURE FLAGS] Signature verification failed — using code defaults');
return { ...DEFAULT_FEATURE_FLAGS };
}
return { ...DEFAULT_FEATURE_FLAGS, ...received };
Comment on lines +29 to +51
/**
* Authoritative server-side check for a single flag. Use this to enforce a gate on a dedicated API
* endpoint (mirrors `checkUserEntitlement`). Most of the UI hits generic endpoints, so this is only
* useful for the rare feature with its own server route.
*/
/**
* Canonical "active team" used for flag scoping: an ACTIVE membership in an ACTIVE team. The /api/me
* profile path (`findIdByUserIdUserFacing`) applies the same rule in-memory off its already-loaded
* membership, so the flags shown in the UI and the server-side gate always agree on the team.
*/
export async function resolveActiveTeamIdForUser(userId: string): Promise<string | null> {
const membership = await prisma.teamMember.findFirst({
select: { teamId: true },
where: { userId, status: 'ACTIVE', team: { status: 'ACTIVE' } },
});
return membership?.teamId ?? null;
}

export async function checkFeatureFlag({ userId, key }: { userId: string; key: FeatureFlagKey }): Promise<boolean> {
const teamId = await resolveActiveTeamIdForUser(userId);
const flags = await resolveFeatureFlagsForUser({ userId, teamId });
return flags[key];
}
Comment on lines +2 to +14
CREATE TABLE "feature_flag_override" (
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
"key" VARCHAR(255) NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT true,
"userId" UUID,
"teamId" UUID,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "feature_flag_override_pkey" PRIMARY KEY ("id")
);

-- CreateIndexes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants