feat: per-tier feature toggles for recipe variations/pairings (v0.50.0)

Admins can now disable specific AI features per tier from Admin > Tier
Limits — new feature_flags table (feature x tier -> enabled, defaulting
to true so adding a new gated feature never needs a backfill).

Covers recipe variations, drink pairing, and meal pairing to start.
When disabled for a user's tier, the button stays visible (with a small
lock badge) but opens an upgrade dialog instead of running; the API
route rejects the call server-side either way (requireFeatureEnabled,
re-reads tier from the DB rather than trusting the session's cache,
same rationale as checkAndIncrementTierLimit).

The upgrade dialog is informational only — no Stripe checkout exists
yet (STRIPE_PLAN.md is still just a plan) — its CTA links to /support
prefilled as an upgrade-interest suggestion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-18 23:35:12 +02:00
parent 12c2ec213a
commit 2f3ba14093
24 changed files with 5945 additions and 19 deletions
+20
View File
@@ -0,0 +1,20 @@
import { pgTable, text, timestamp, boolean, primaryKey } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { users, tierEnum } from "./users";
/** Absence of a row for a (featureKey, tier) pair means "enabled" — rows only
* exist to record an override, so adding a new gated feature never requires
* a backfill. */
export const featureFlags = pgTable("feature_flags", {
featureKey: text("feature_key").notNull(),
tier: tierEnum("tier").notNull(),
enabled: boolean("enabled").notNull().default(true),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
updatedById: text("updated_by_id").references(() => users.id, { onDelete: "set null" }),
}, (t) => [
primaryKey({ columns: [t.featureKey, t.tier] }),
]);
export const featureFlagsRelations = relations(featureFlags, ({ one }) => ({
updatedBy: one(users, { fields: [featureFlags.updatedById], references: [users.id] }),
}));
+1
View File
@@ -8,3 +8,4 @@ export * from "./messaging";
export * from "./billing";
export * from "./ai-chat";
export * from "./support";
export * from "./feature-flags";