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:
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE "feature_flags" (
|
||||
"feature_key" text NOT NULL,
|
||||
"tier" "tier" NOT NULL,
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_by_id" text,
|
||||
CONSTRAINT "feature_flags_feature_key_tier_pk" PRIMARY KEY("feature_key","tier")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "feature_flags" ADD CONSTRAINT "feature_flags_updated_by_id_users_id_fk" FOREIGN KEY ("updated_by_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -330,6 +330,13 @@
|
||||
"when": 1784369252969,
|
||||
"tag": "0046_curly_sabretooth",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 47,
|
||||
"version": "7",
|
||||
"when": 1784394631882,
|
||||
"tag": "0047_acoustic_goblin_queen",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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] }),
|
||||
}));
|
||||
@@ -8,3 +8,4 @@ export * from "./messaging";
|
||||
export * from "./billing";
|
||||
export * from "./ai-chat";
|
||||
export * from "./support";
|
||||
export * from "./feature-flags";
|
||||
|
||||
Reference in New Issue
Block a user