1. replaceAll (Find & Replace) — une seule transaction ProseMirror au lieu d'un forEach cassé. Tous les matchs sont maintenant remplacés. 2. Link Preview unwrap — deleteNode() au lieu de clearer les attrs qui laissaient un nœud fantôme invisible dans le document. 3. Conversion Markdown → richtext — breaks: true dans marked.parse() Les simple newlines sont maintenant convertis en <br>. + préserve les blocs custom (toggle, callout, math, columns, outline, link-preview) en commentaires HTML lors de l'export MD. 4. emitNoteChange exercices — shape corrigée (type:'created' attend un objet Note, pas noteId/notebookId séparés). 5. Raccourcis clavier sans conflit : Cmd+Shift+C → Cmd+Alt+C (callout, avant: copier) Cmd+Shift+O → Cmd+Alt+O (outline, avant: historique/signets) Cmd+Shift+L → Cmd+Alt+L (colonnes, avant: lock screen macOS)
11 KiB
title, type, created, status, baseline_commit, context
| title | type | created | status | baseline_commit | context | |||
|---|---|---|---|---|---|---|---|---|
| Subscription & Quota Admin Management | feature | 2026-06-20 | done | 5b13a88b72 |
|
Intent
Problem: Subscription tiers, AI quotas, and Stripe business config are split between hardcoded code (TIER_LIMITS), server .env (price IDs, billing flag), and a minimal admin override (tier dropdown only). Operators cannot adjust limits or billing settings without redeploy; quota enforcement has a TOCTOU race (check then increment); token analytics are partially broken.
Approach: Add a secure admin Billing & Quotas console backed by DB (PlanEntitlement + SystemConfig), keep Stripe secrets in infra env only, unify quota enforcement on atomic reserveUsageOrThrow, and expose usage dashboards with audit logging for manual tier overrides.
Boundaries & Constraints
Always:
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETstay in server env / secrets vault — never inSystemConfig, never editable from browser.- Stripe remains authoritative for paid subscriptions; webhooks drive state; manual admin tier changes are support overrides only.
- Quota gating unit stays requests per feature per calendar month (not token-based blocking).
- Redis quota checks fail-open with structured error logging (industry standard for AI rate limits).
- All admin mutations require
role === ADMIN; i18n for new UI strings inlocales/en.jsonandlocales/fr.jsonminimum. - Non-destructive DB migration only; seed
PlanEntitlementfrom currentTIER_LIMITSdefaults.
Ask First:
- Adding new Prisma models beyond
PlanEntitlement(e.g. fullFeatureFlagwiring). - Changing Stripe products/prices in live Stripe Dashboard (ops action, not code).
- Switching to usage-based Stripe Meter / Metronome billing.
Never:
- Store
sk_*orwhsec_*in plaintext DB or admin forms. - Run
prisma migrate reset,db push --accept-data-loss, or any destructive DB command. - Replace Redis real-time counters with synchronous PostgreSQL writes on every AI request.
- Add automated tests unless explicitly requested later.
I/O & Edge-Case Matrix
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|---|---|---|---|
| Admin saves tier limit | ADMIN edits PRO chat limit 50→75 |
PlanEntitlement upserted; next getLimit() returns 75; existing Redis counters unchanged until period rollover |
403 if non-admin; 400 if invalid feature or negative limit |
| Admin saves Stripe price ID | ADMIN sets STRIPE_PRICE_PRO_MONTHLY in SystemConfig |
resolvePriceId('PRO','month') reads DB value; env used only as fallback |
400 if empty when billing enabled |
| User at quota | User on BASIC, 30/30 semantic_search used |
reserveUsageOrThrow returns 402 QUOTA_EXCEEDED |
Fail-open if Redis down (allow + log alert) |
| Concurrent AI requests | Two requests at limit−1 | Only one succeeds; second gets 402 (atomic Lua) | N/A |
| Admin manual tier override | ADMIN sets user to BUSINESS | Subscription upserted; AuditLog entry with actor, target, old/new tier |
Cannot override own tier (existing rule) |
| Billing disabled | BILLING_ENABLED=false in SystemConfig |
/settings/billing shows disabled state; checkout buttons hidden |
N/A |
| suggest-charts usage | Successful chart suggestion | incrementUsageAsync called once (not broken 4-arg call) |
N/A |
Code Map
memento-note/prisma/schema.prisma— addPlanEntitlementmodel (tier,feature,limitValue; unique on[tier, feature];nulllimitValue = unlimited; omit row or sentinel = feature unavailable per tier)memento-note/prisma/migrations/*— additive migration + seed from currentTIER_LIMITSmemento-note/lib/entitlements.ts— load limits from DB with in-memory cache (TTL ~60s) and hardcoded fallback; exportgetLimit()async; keep fail-open on Redis errorsmemento-note/lib/billing/stripe-prices.ts— read price IDs viagetConfigValue()with env fallback; remove sole dependence onprocess.envmemento-note/lib/config.ts— add billing keys toENV_FALLBACKS(BILLING_ENABLED, fourSTRIPE_PRICE_*)memento-note/app/actions/admin-billing.ts— server actions:getPlanEntitlements,updatePlanEntitlement,updateBillingConfig,getUsageOverviewmemento-note/app/(admin)/admin/billing/page.tsx— admin UI: tier limits matrix, billing config, usage summary tablememento-note/app/(admin)/admin/billing/billing-admin-form.tsx— client form componentmemento-note/components/admin-sidebar.tsx— nav link/admin/billingmemento-note/app/actions/admin.ts— logSUBSCRIPTION_OVERRIDEaudit onupdateUserSubscriptionmemento-note/lib/audit-log.ts— extendAuditActionwithSUBSCRIPTION_OVERRIDE,BILLING_CONFIG_UPDATED,PLAN_ENTITLEMENT_UPDATEDmemento-note/components/settings/billing-plans.tsx— readbillingEnabledfrom/api/billing/statusinstead of build-time env onlymemento-note/app/api/billing/status/route.ts— includebillingEnabledfrom SystemConfigmemento-note/app/api/ai/suggest-charts/route.ts— fix usage tracking (useincrementUsageAsync, not brokentrackFeatureUsagecall)- AI routes using
checkEntitlementOrThrow+incrementUsageAsync— migrate toreserveUsageOrThrowonly (remove duplicate increment on success); include chat route
Tasks & Acceptance
Execution:
prisma/schema.prisma+ migration — addPlanEntitlement, seed defaults fromTIER_LIMITS— DB-backed limits without breaking existing userslib/entitlements.ts— async limit loader with cache + fallback; updatecanUseFeature,reserveUsageOrThrow,getUserQuotas— single source of truthlib/billing/stripe-prices.ts+lib/config.ts— SystemConfig-backed price IDs andBILLING_ENABLED— admin-editable business configapp/actions/admin-billing.ts— CRUD entitlements + billing config + usage overview query (joinUsageLog+ Redis snapshot) — secure admin API surfaceapp/(admin)/admin/billing/*+components/admin-sidebar.tsx— admin UI section — operator-facing managementapp/actions/admin.ts+lib/audit-log.ts— audit trail for tier overrides and config changes — support accountability- AI routes (grep
checkEntitlementOrThrow) +app/api/chat/route.ts— switch toreserveUsageOrThrow; drop redundantincrementUsageAsyncon success — fix TOCTOU race app/api/ai/suggest-charts/route.ts— fix broken usage call — restore quota accountingapp/api/billing/status/route.ts+components/settings/billing-plans.tsx— runtime billing flag — no redeploy to toggle billing UIlocales/en.json+locales/fr.json— i18n keys underadmin.billing.*— FR/EN reference labels
Acceptance Criteria:
- Given an ADMIN on
/admin/billing, when they change PROchatlimit and save, then a new entitlement check within 60s reflects the new limit without app restart. - Given a BASIC user at quota, when two parallel AI requests arrive, then at most one succeeds and the other returns HTTP 402.
- Given
BILLING_ENABLEDis false in SystemConfig, when a user opens/settings/billing, then checkout/upgrade actions are hidden and a clear disabled message is shown. - Given an ADMIN changes a user's tier from
/admin/users, when the change succeeds, then anAuditLogrow records actor, target user, old tier, and new tier. - Given Stripe price IDs are set in SystemConfig (env unset), when checkout is initiated, then the correct Stripe price is used.
- Given Redis is unreachable, when an AI request is made, then the request is allowed (fail-open) and an error is logged server-side.
- Given
STRIPE_SECRET_KEYremains env-only, when inspectingSystemConfigtable, then nosk_orwhsec_values exist.
Design Notes
PlanEntitlement limit encoding: limitValue: null → unlimited; positive integer → monthly request cap; omit row (or explicit -1 sentinel if needed) → feature not available on tier (maps to current undefined in TIER_LIMITS).
Cache invalidation: After admin saves entitlements, call a small invalidateEntitlementCache() so changes apply immediately without waiting for TTL.
Quota migration pattern: Replace await checkEntitlementOrThrow(userId, feature) + incrementUsageAsync(...) with await reserveUsageOrThrow(userId, feature) at request start. Do not increment again on success. For streaming chat, reserve before streamText; no post-finish increment.
Usage dashboard: Show per-tier aggregate from UsageLog (last synced month) plus optional live Redis read for current month top features — degraded state if sync cron stale is acceptable (show syncedAt).
Verification
Commands:
cd memento-note && npm run test:unit -- tests/unit/entitlements.test.ts— expected: pass after async limit loader changescd memento-note && npx tsc --noEmit— expected: no type errors
Manual checks:
- ADMIN
/admin/billing: edit a limit, save, verify/api/usage/currentreflects new cap for a test user on that tier. - Non-admin GET to admin-billing server actions returns unauthorized.
- Two rapid AI calls at quota−1: only one succeeds.
Spec Change Log
Suggested Review Order
Entitlements & DB limits
-
Central loader: DB
PlanEntitlementwith 60s cache and hardcoded fallbackplan-entitlements.ts:1 -
Prisma model + seeded migration from former
TIER_LIMITSschema.prisma:814 -
Entitlements API now reads limits asynchronously from cache
entitlements.ts:1
Admin console
-
Server actions: entitlements CRUD, billing config, usage overview
admin-billing.ts:1 -
Admin UI at
/admin/billingwith tier matrix + Stripe price IDsbilling-admin-client.tsx:1 -
Audit trail on manual tier override
admin.ts:142
Stripe & billing UX
-
Price IDs and
BILLING_ENABLEDvia SystemConfig (env fallback)stripe-prices.ts:79 -
Runtime billing flag exposed to settings page
billing-plans.tsx:80
Quota hardening
-
Atomic
reserveUsageOrThrowon billable AI routes (replaces check+increment)chat/route.ts:69 -
Fixed broken suggest-charts usage tracking
suggest-charts/route.ts:58
Tests
- Entitlements + billing price map unit tests updated
entitlements.test.ts:1