Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ FLUENT_AI_KEY=fai_dev_admin
# (e.g. POST /tools/greek-room/repeated-words), so leave this empty. If/when
# fluent-ai adopts versioned routing, set this to /api/v1 — no code change needed.
FLUENT_AI_API_PREFIX=

# Feature flags (EN_FEATURE_*)
# One flat boolean per optional feature, published (camelCased) by
# GET /config/features so the front-end can hide AI-dependent UI where the
# backend isn't wired. Accepted: true/false, 1/0, yes/no, on/off.
#
# EN_FEATURE_REPEATED_WORD_CHECK — the Repeated Word Check UI. Leave UNSET to
# derive the safe default: on only when FLUENT_AI_URL + FLUENT_AI_KEY are both
# set, off otherwise. Set explicitly to force it either way.
# (A flag is declared in three synced places: src/env.ts, the FLAGS registry in
# src/lib/features.ts, and the OpenAPI schema in src/routes/config.route.ts.)
EN_FEATURE_REPEATED_WORD_CHECK=
3 changes: 3 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@ npx tsx src/db/seeds/bibles.ts || { echo "ERROR: bibles seed failed"; exit 1; }
echo "Seeding bible texts..."
npx tsx src/db/seeds/bible-texts.ts || { echo "ERROR: bible texts seed failed"; exit 1; }

echo "Seeding pericope sets..."
npx tsx src/db/seeds/pericope-sets.ts || { echo "ERROR: pericope sets seed failed"; exit 1; }

echo "Starting dev server..."
exec npx tsx watch src/index.ts
316 changes: 316 additions & 0 deletions docs/proposals/feature-flags/feature-flags-suggestion.md

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions docs/proposals/feature-flags/feature-flags-summary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Feature Flags — Review Summary

**Status:** Reviewed and implemented. Q1–Q4 resolved by kaseywright (2026-07-07);
see "Reviewer outcome" below. Shipped in fluent-api #213 / fluent-web #337. One
change from the draft: `GET /config/features` is **authenticated** (login-only,
no role), not unauthenticated.

**Purpose:** Reviewer orientation for a lightweight feature-flag mechanism. The
full design is in the sibling [`feature-flags-suggestion.md`](feature-flags-suggestion.md)
(problem, design, decisions **D1–D8**, open questions **Q1–Q4**). Ships as a
coordinated pair of PRs — fluent-api (flag source + endpoint) and fluent-web
(consumer hook, gate primitive, diagnostics page).

## The problem

fluent-ai is **not hosted** in any deployed environment. Every PR that depends on
it therefore can't merge — doing so would surface UI whose backend isn't there
and block promoting to production, which stalls unrelated work queued behind it.
We need to **turn AI-dependent front-end features on/off per environment** so
their PRs can merge and ride to production **hidden**, then be switched on later
(where fluent-ai actually runs) with a config change and no code redeploy. This
is a **wiring/deploy concern**, not application data.

## What's being proposed

1. **Env-sourced flags, one flat var per flag** under a specific `EN_FEATURE_*`
prefix (e.g. `EN_FEATURE_REPEATED_WORD_CHECK`), each **declared explicitly in
the Zod env schema** — the schema doubles as the authoritative flag catalog
and survives Zod's unknown-key stripping. No database (**D1, D2**).
2. **A read-only endpoint `GET /config/features`** — a new **login-gated** meta
route (sibling of `/health`; `authenticateUser`, no role, 401 without a
session) returning a **named map**,
`{ features: { repeatedWordCheck: true } }`, assembled from the `EN_FEATURE_*`
vars (prefix stripped, camelCased). New flags are purely additive (**D3, D4**).
3. **fluent-api owns the truth, publishes a projection.** The env is the single
source of truth; the endpoint is the publication channel, not a second
source. fluent-web never decides policy — it only reflects.
4. **Safe default.** The repeated-word-check flag defaults **off** unless
`FLUENT_AI_URL` + `FLUENT_AI_KEY` are both wired, so forgetting to set it in
an AI-less environment yields the safe answer (**D2**).
5. **fluent-web consumes it** via a small extensible primitive — a
`useFeatureFlags()` TanStack Query hook + a `<FeatureGate>` wrapper over a
`Record<name, boolean>`. Gated AI UI **fails closed (hidden)** while loading
or on endpoint error (**D6, D7**).
6. **An unlinked, login-gated, read-only diagnostics page** (a `chrome://`-style
catch-all for technical/health details, starting with the flag map). Reuses
the existing `_authenticated` gate; no role check (**D8**).

## Explicitly out of scope

- **No server-side enforcement.** Turning a flag off does **not** gate the AI
endpoints — publishing is deliberately decoupled from the request path. If AI
is unconfigured, the AI call fails on its own (a pre-existing, separate
consequence). (**D5**)
- No database, no runtime toggling UI, no per-user/tenant targeting, no
percentage rollouts. This is a per-environment on/off wiring switch.

## Areas where input was most valuable (all resolved)

1. **Endpoint auth (Q1).** `GET /config/features` was proposed unauthenticated;
the reviewer chose the floated alternative — **require a session**.
2. **Route vs. `/health` (Q2).** Whether to fold a `features` block into
`/health` instead of a dedicated route.
3. **Prefix & key naming (Q3).** `EN_FEATURE_*` prefix,
`EN_FEATURE_REPEATED_WORD_CHECK` → `repeatedWordCheck`, and per-flag schema
declaration.
4. **Default-derivation (Q4).** Whether to keep the AI-wiring-derived unset
default or use a plain `false`.

## Reviewer outcome

Resolved by **kaseywright** on **2026-07-07** (proposal PR #211 review +
follow-up on impl PR #213):

- **Q1 → authenticate.** `GET /config/features` is **login-gated**
(`authenticateUser`, no role; 401 without a session) — the one change from the
draft. Only signed-in SPA users need the flag map.
- **Q2 → dedicated `GET /config/features`.** Kept product-config semantics off
the liveness probe.
- **Q3 → confirmed as-is.** `EN_FEATURE_*` prefix + `repeatedWordCheck` key +
explicit per-flag schema declaration accepted.
- **Q4 → keep the AI-wiring-derived default.** Retained as defensive
belt-and-suspenders (an earlier lean toward a plain `false` was reversed by
the reviewer on #213); explicit `EN_FEATURE_REPEATED_WORD_CHECK=false` is the
operator override.
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import configureOpenAPI from '@/lib/configure-open-api';
import { server } from '@/server/server';
import '@/routes/index.route';
import '@/routes/health.route';
import '@/routes/config.route';
import '@/routes/auth-doc.route';
import '@/domains/users/users.route';
import '@/domains/users/chapter-assignments/users-chapter-assignments.route';
Expand All @@ -22,6 +23,7 @@ import '@/domains/users/projects/user-projects.route';
import '@/domains/chapter-assignments/presence/chapter-assignments-presence.route';
import '@/domains/ai-tools/ai-tools.route';
import '@/domains/pericopes/pericopes.route';
import '@/domains/self/settings/self-settings.route';
configureOpenAPI(server);

export default server;
8 changes: 8 additions & 0 deletions src/db/migrations/0013_add_user_settings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE "user_settings" (
"user_id" integer PRIMARY KEY NOT NULL,
"settings" jsonb,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now()
);
--> statement-breakpoint
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
2 changes: 1 addition & 1 deletion src/db/migrations/meta/0012_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,7 @@
"nulls": "last"
}
],
"isUnique": false,
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
Expand Down
Loading
Loading