Adds inline loading feedback to OAuth sign-in, fixes the license checkout flow end-to-end (user_id now reaches the Stripe webhook so purchases are no longer silently dropped), makes the my-account billing card billing-model-aware, pins the Next.js release-train packages, and lands the full code-review v2 remediation across security, architecture and performance.
OAuth sign-in buttons on the login page now show an inline spinner and a localized "Connecting to {provider}…" label, and disable all providers while one is mid-redirect (components/auth/oauth-buttons.tsx). signInWithOAuth makes a network round-trip to resolve the provider authorization URL before the browser navigates away, so the buttons previously gave no feedback during that gap and looked unresponsive. Added aria-busy on the active button and a guard against double-clicks / racing two providers; on error the state resets so the user can retry, on success the spinner stays up since the page is already navigating
License checkout sessions now carry user_id in their Stripe metadata (createLicenseCheckoutSession in core/billing/checkout.ts). The checkout.session.completed handler guards on missing account_id/user_id, so every license purchase was silently dropped at the webhook ("Checkout session missing required metadata") — the license was never created and credits never granted. This affected both the dashboard LicensePricingTable and the landing-page flow. user_id now also populates licenses.purchased_by and the payments audit row (both previously null)
Landing-page license purchases now complete end-to-end. checkout-client.tsx gained a type: 'license' branch that POSTs to /api/billing/license-checkout (previously licenses fell through to the !priceId guard and threw "Payment configuration not available for this plan" — licenses carry a productId, not a Stripe priceId)
landing-licenses.tsx now sets the bsk_pending_checkout httpOnly cookie (parity with the subscription flow, so cross-tab magic-link logins keep the selection) and uses an allowlisted locale-less redirectTo=/checkout. The prior locale-prefixed, query-bearing redirectTo was rejected by sanitizeRedirectTo and bounced logged-out buyers to /private-dashboard before they reached checkout
The /my-account billing card is now billing-model-aware: in license mode it shows "License" / "Manage your license and access" / "No active license" copy instead of the subscription-centric strings, and the license-access badge renders the localized product name (from pricingConfig.licenses) instead of the raw product id. The subscription and hybrid modes keep the subscription wording
Pinned the Next.js release-train packages to ^16.2.9 (from ^16.2.6): next, eslint-config-next, and @next/bundle-analyzer. next already resolved to 16.2.9 (latest 16.2.x), so this only raises the declared floor so a fresh install can't drop to 16.2.6–16.2.8; @next/bundle-analyzer was the one real upgrade. All three bumped in lockstep to avoid ESLint-config / analyzer skew against the framework. Does not clear the build-time-only postcss advisory (GHSA-qx2v-qp2m-jg93), which only a future Next release can resolve
CRITICAL — fixed the stale admin_logs INSERT policy in supabase/schema.sql's bottom migrations-mirror block (ARCH-1): the block re-created the pre-SEC-M4 is_admin WITH CHECK policy, so any fresh supabase db reset (dev, CI, downstream forks) silently re-opened the audit-log forgery hole patched by 20260610090002. The mirror now re-creates the service-role-only policy. Incrementally-migrated environments were never affected
OAuth account-merge is now atomic (SEC-4): a new merge_oauth_duplicate_user SECURITY DEFINER RPC (service-role-only EXECUTE + auth.role() guard) deletes the duplicate user's personal account/profile and backfills OAuth display data in one transaction. auth.admin.deleteUser runs only after commit; if it fails, the flow still completes as "account linked" (orphan auth row logged) instead of re-creating rows for the merged-away user
CSRF cookie sameSite relaxed from 'strict' to 'lax' at all three set sites (SEC-3) — 'strict' dropped the cookie on cross-site top-level navigations (email links), desyncing the double-submit pair into spurious 403s. The token is non-secret; Origin/Referer validation and the double-submit comparison are unchanged. Added an architecture-invariants test (SEC-5) pinning the load-bearing UNIQUE(referred_account_id) constraints and the self-referral CHECK so a refactor can't silently drop the anti-farming guarantee
Consolidated the licensing domain (ARCH-2/3): the unified access check's batch license read and the admin licenses list now go through new core/licenses/queries.ts readers; deleted 9 dead license functions plus unused types. Removed the unwired core/ export cluster across accounts/affiliates/roles/chat (ARCH-4); live chat readers dropped their misleading Uncached suffix. Role checks consolidated onto hasRole() and the post-auth redirect swap unified behind getPostEventRedirect() (ARCH-5/6); invitation expiry now derives from workspaceConfig and resendInvitation() moved to core with the Zod validation it previously lacked (ARCH-7/8/11)
Rewrote the generate-analytics job (PERF-1/2): the daily AI usage summary now calls the get_admin_ai_period_stats aggregate RPC instead of downloading every ai_requests row of the day and reducing in JS, and its three independent reads run in Promise.all. check-low-credits-alerts moved its balance filter into the DB at the widest configured threshold (PERF-3); cleanup-push-subscriptions runs its three independent deletes concurrently (PERF-4)
v1.31Patch
June 10, 2026
Full-codebase review remediation — security, architecture, performance, and database
A broad remediation pass from a full-codebase review: recent-auth gates and Zod validation on destructive admin routes, service-role-only audit-log writes, an extended SSRF blocklist and SVG removal from the public bucket, a restored dual-column admin RLS check, redundant-index cleanup and new hot-path composites, ~100k-row admin-analytics aggregations moved into SQL RPCs, duplicate-auth elimination on chat/referral routes, and a sweep of architecture and i18n fixes.
Added Zod validation (discriminated union, UUID licenseId, bounded days) and the requireRecentAuth() gate to the admin license revoke/extend route — revoking a license claws back credits and paid access, so a stale hijacked admin session can no longer do it in a single POST. Added requireRecentAuth() to the referral reversal and referral-code deactivation routes too, closing the drift with the structurally identical affiliate conversion-reverse route
Restricted admin_logs INSERT to service_role (20260610090002): the old is_admin WITH CHECK let any admin forge audit rows attributing actions to another admin via the Data API. All legitimate writes already went through supabaseAdmin; SELECT stays admin-readable
Stopped leaking raw Postgres/RPC error text from the four admin cron routes — clients now get a generic error plus a code while the full message stays in error_logs. Newsletter subscribe/unsubscribe no longer return the email provider's raw error string on 500, and the unsubscribe provider-failure path is now logged
Extended the SSRF private-IP blocklist (lib/security/ssrf.ts) with the CGNAT shared range 100.64.0.0/10 (RFC 6598), 192.0.0.0/24 (RFC 6890), and 198.18.0.0/15 (RFC 2544) — admin-configured webhook URLs can no longer reach those ranges
Removed image/svg+xml from the public media bucket (20260610090006) and the app-side allowlist, deleting the bypassable regex-based SVG content filter with it. SVGs can carry scripts and the bucket is public; use PNG/WebP or serve trusted SVGs from /public
Fixed user_is_account_admin dropping the role_slug branch (20260610090000): the May 29 search_path-pinning migration re-created the function with only the legacy role check, silently denying member management to custom-role admins whose membership only sets role_slug. Restored the dual-column check everywhere, including the later CREATE OR REPLACE that wins on a fresh db reset
Dropped redundant/overlapping indexes (20260610090005, 20260610090008) to cut write amplification, and added composite hot-path indexes via standalone CONCURRENTLY migrations: api_keys(account_id, created_at DESC), documents(account_id, created_at DESC), and referrals(ip_hash, created_at DESC) backing the apply_referral_code IP rate-limit count
Replaced the admin analytics JS aggregation (~100,000 ai_requests rows per /admin-dashboard/analytics render) with 8 SQL aggregate RPCs (20260610090001): get_admin_ai_period_stats, get_admin_active_user_count, get_admin_credit_flow, get_admin_daily_stats, get_admin_top_users, get_admin_model_stats, get_admin_agent_stats, get_admin_org_stats — all SECURITY DEFINER, service-role-only EXECUTE, auth.role() body-guarded
Killed the double auth.getUser() + duplicate memberships fetch on 8 chat-session and referral routes: new checkPermissionForUser(userId, accountId, permission) reuses the user apiSecurity.* already authenticated — ~60–120 ms saved per request
Bounded the unbounded list queries (listDocumentsForAccount capped at 200 with metadata dropped from the list projection, api_keys capped at 100), parallelized the admin dashboard layout's independent awaits, and moved isomorphic-dompurify (~150 KB) out of the CMS page form's client chunk into a dynamic ssr:false preview component. Raised the Lighthouse CI performance floor from 0.7 to 0.85
Replaced 7 inline dual-column owner checks with the shared hasRole() helper (my-account, admin layout, subscriptions/organizations/licenses/org-detail pages). This surfaced and fixed two latent bugs: the licenses page checked role_slug only and the org-detail page checked role only — both queries now select both columns
Extracted the inline service-role orchestration from the three heaviest admin pages into core modules: users → core/organizations/admin-users.ts, organizations → core/organizations/admin-organizations.ts, subscriptions → core/billing/admin-subscriptions.ts. Behavior-preserving; pages are now thin Server Component shells
Fixed locale-less number/date formatting on the affiliates dashboard KPIs + conversions table, ReferralStatsRow, and landing-licenses.tsx (now via getIntlLocale(locale)). Wired the previously-unwired createInAppNotification producer: the check-low-credits-alerts job now creates a bell notification for the account owner alongside the push
Updated the canonical AI rules and user-facing docs to match the new code reality (recent-auth action list, analytics RPC family, SVG ban, checkPermissionForUser() guidance, Lighthouse 70% → 85% target) and synced the .codex / .gemini / .opencode rule mirrors
Closes an in-tenant privilege-escalation path on memberships (a non-owner admin could mint an owner via direct Data API insert), scopes the GDPR data export's api_keys section to owners/admins, and adds an edge-safe auth wrapper so missing or expired Supabase sessions are treated as anonymous instead of throwing. Plus an init-script fix so npm run init no longer drops the CDN config block.
Closed an in-tenant privilege-escalation path on memberships: the owner-mint guard (lock_membership_sensitive_columns) was bound BEFORE UPDATE only, while the 'Admins can insert memberships' RLS policy checks account-admin without constraining the inserted role — so a non-owner workspace admin could POST /rest/v1/memberships with role='owner' for a colluding user and escalate them to owner (billing + deletion powers). The trigger is now BEFORE INSERT OR UPDATE and rejects owner inserts/updates from Data API callers
Hotfixed the guard's role scope: the first cut used auth.role() is distinct from 'service_role', which caught the SECURITY DEFINER signup trigger handle_new_user_account() (it runs on the GoTrue auth-admin connection where auth.role() is NULL) and aborted signup with 'Database error saving new user'. Re-scoped to auth.role() in ('authenticated','anon') so the escalation is still blocked for direct PostgREST writes while internal triggers and service_role bootstrap are exempt
Scoped the GDPR data export's api_keys section to accounts where the requester is owner/admin (via getOrgManagerRoles() + hasRole()), matching the management-UI gate (requireApiKeyManager) so a plain member can no longer export key names/prefixes/scopes of keys they cannot otherwise see
Added lib/auth/safe-get-user.ts (safeGetUser) — an edge-safe wrapper that treats a missing/expired/invalid Supabase session as anonymous instead of letting @supabase/ssr throw AuthApiError: Invalid Refresh Token (which surfaced as an unhandled 500 / log noise). Wired into every server-side auth.getUser() site (get-user.ts, proxy.ts, api-security.ts, permissions/check.ts, verify-admin.ts, stripe/actions.ts, ai/stream). Fails closed — protected routes still redirect, authenticated routes still return 401
Fixed scripts/init-project.js regenerating config/app.ts without the cdn block and the isPublicCacheablePath() / PUBLIC_CACHEABLE_PATHS exports. The CDN feature (1.29) added these to the committed config, the wizard prompt, and .env.example, but the generateAppConfig() template was never updated — so running npm run init overwrote config/app.ts minus 57 lines, breaking proxy.ts
Mirrored the cdn block, PUBLIC_CACHEABLE_PATHS, and isPublicCacheablePath() into the generateAppConfig() template so future inits emit a complete config, and added anti-pattern A15: any field/helper added to a config file that init-project.js regenerates MUST be mirrored into the matching generate*() template in the same change — treated as one edit unit, like the migration↔schema.sql mirror (A1)
v1.29Minor
May 29, 2026
Security, jobs, and database performance hardening, dynamic-slug rendering, optional CDN caching, and SEO/localization
A broad hardening pass across security, background jobs and database performance: tighter membership and account-deletion policies, centralized webhook SSRF validation, service-role-only Stripe sync, aggregate RPCs for dashboard counters, and pinned SECURITY DEFINER search paths. Plus a production blog/CMS dynamic-slug rendering fix, an opt-in CDN edge-caching layer, and SEO/localization improvements (server-rendered html lang, normalized site URLs, better locale negotiation).
Hardened memberships updates with WITH CHECK plus a trigger that blocks non-service-role account/user reassignment and owner self-escalation through either role or role_slug
Refactored /api/admin/jobs/** into thin routes backed by core/jobs/*, centralized webhook SSRF validation in lib/security/ssrf.ts, and documented the shared create-time/runtime DNS checks
Reworked sync-stripe to batch existing subscription state updates through a service-role-only RPC, while keeping missing-row creation inside the billing domain flow
Added aggregate RPC usage for admin/org AI statistics and documented the no-raw-row-download pattern for dashboard counters
Memoized CMS unstable_cache() wrappers per canonical key and documented the pattern for future dynamic-key caches
Tightened GDPR export failures so internal section messages are logged server-side and the client receives a generic EXPORT_FAILED response
Closed an in-tenant privilege-escalation path on account_deletion_requests: the owner-cancel UPDATE policy now carries a WITH CHECK, and a BEFORE UPDATE column-lock trigger restricts non-service-role owners to a status → 'cancelled' transition so a member or owner can no longer flip cascade_members (or any other field) via the Data API to force member-account deletion
Pinned SET search_path = public on the SECURITY DEFINER membership helpers (user_belongs_to_account, user_is_account_admin, user_has_permission) and the lock_membership_sensitive_columns / lock_notification_columns triggers
Added a matching WITH CHECK to the storage.objects 'Admins can update media' policy so an admin cannot move a media object out of the media bucket
Narrowed the pending_emails.email_type CHECK to the worker-supported set (contact, newsletter, notification) so unsupported types fail loudly at insert instead of being silently permanent-failed
Consolidated the admin jobs-URL SSRF validation in core/admin/settings.ts onto the canonical validatePublicWebhookUrl resolver (removed a drifting regex/DNS copy)
Replaced the admin overview revenue JS-reduce with a get_admin_revenue_sum aggregate RPC and wrapped getPlatformStats in unstable_cache (60s, per-locale)
Dropped redundant single-column indexes covered by composites (credit_tx_account_idx, subscriptions_account_idx) and added composites for the revenue (payments(status, created_at desc)) and job-run history (job_runs(job_id, started_at desc)) hot paths
Moved the documents/[id] detail route to a thin dispatch over a new core/documents reader, memoized the per-post blog cache wrapper, dropped a redundant getUser() round-trip on onboarding (now via SecurityContext.user_metadata), parallelized the email-provider per-list loops, removed ~20 stale as any casts on now-typed tables, and gave in-app notification helpers a core/notifications/ home
Fixed a production DYNAMIC_SERVER_USAGE 500 on blog posts / CMS pages published after the last build: blog/[slug] and [slug] are now force-dynamic (data still cached via unstable_cache) so on-demand ISR no longer conflicts with the root layout's per-request CSP-nonce headers() read
Added an opt-in CDN edge-caching layer (CDN_PUBLIC_CACHE_ENABLED, CDN_PUBLIC_S_MAXAGE, CDN_PUBLIC_SWR): when enabled, the middleware advertises a cacheable Cache-Control on anonymous GET requests to public content pages only (no auth cookie, no Set-Cookie), keeping the strict per-request CSP nonce intact while authenticated/dashboard/API responses stay private, no-store. Off by default; documented in caching.md, .env.example, and the production caching docs
Server-rendered the actual route locale on <html lang> (fr-FR, fr-CH, en-US, en-CA) instead of patching a default language after hydration, improving crawler and accessibility-tool accuracy
Centralized site URL normalization for canonical URLs, hreflang alternates, OpenGraph/JSON-LD, sitemap, and robots metadata so trailing slashes in NEXT_PUBLIC_APP_URL no longer produce duplicate slashes
Improved locale negotiation for unprefixed requests so Accept-Language can redirect to exact configured regional locales such as en-CA
v1.28Patch
May 27, 2026
Private sidebar credits widget sync, explicit Supabase Data API grants, and refreshed Supabase agent skills
The private dashboard sidebar credits widget now stays in sync with server-authoritative props after server actions like End Trial; a new migration makes Supabase Data API grants explicit for fresh projects and database resets, with documentation refreshed; local Supabase agent skills and AI-tool entrypoints are updated to invoke both the product-wide Supabase skill and the Postgres best-practices skill.
Private dashboard sidebar credits widget now updates immediately after server-side credit changes (notably the End Trial confirmation) without requiring a full page reload
Root cause fix: useState(initialCreditsBalance)'s lazy initializer only ran on mount, so router.refresh() re-rendered the layout with a new server-authoritative prop while the sidebar kept stale local state — added a useEffect that re-syncs creditsBalance whenever the initialCreditsBalance prop changes, preserving the existing 'credits-updated' listener for optimistic chat decrements
Org sidebar already read workspace.creditsBalance directly from props, so no change was needed there
New migration 20260527225001_explicit_data_api_grants.sql makes Supabase Data API access explicit for fresh projects and database resets: default public-schema privileges are revoked and intended anon/authenticated/service_role table grants are re-applied, with the full-schema mirrors carrying the same grant surface
Documented the Data API grant model in the Supabase setup docs, onboarding guide, README deployment notes, and AI database rules so future public tables, views, sequences and RPCs ship with grants/revokes alongside RLS policies
Updated local Supabase agent skills from supabase/agent-skills at 4e69c80: added the general supabase skill and refreshed supabase-postgres-best-practices to the upstream 1.1.1 references/ layout
Updated AI-tool entrypoints and generated skill/rule mirrors so future Supabase work invokes both the product-wide Supabase skill and the Postgres best-practices skill where relevant
v1.27Minor
May 27, 2026
Dashboard loading states, protected-layout async reads, and hardened job handlers
Route-segment loading-state guidance for platform admin, organization admin and private dashboards keeps immediate skeleton/spinner feedback while Server Components resolve; the protected dashboard async pattern is documented; background job handlers are hardened with safer GDPR deletion ordering and Stripe subscription mirror recovery.
Added route-segment loading-state guidance for platform admin, organization admin and private dashboard pages so future dashboard work keeps immediate skeleton + spinner feedback while Server Component data resolves
Documented the protected dashboard async pattern: start dashboard-context and translation promises together, await context for redirects, then await translations for rendering without moving user/account data into unstable_cache()
Synced generated AI-rule mirrors for the new dashboard loading and async-read guidance
Hardened background job handlers: GDPR deletion now checks Supabase mutation errors, keeps retry identifiers until destructive work succeeds, and deletes account rows last; Stripe sync creates missing subscription mirrors when create webhooks were missed
v1.26Minor
May 27, 2026
Privacy rights, regional compliance surfaces, and documentation
New customer-facing /[locale]/privacy-choices page and a My Account privacy/data-rights section surface do-not-sell/share, targeted-advertising opt-out, GDPR, Swiss FADP, US state privacy, Canada PIPEDA and CASL rights; cookie consent now honors browser Global Privacy Control signals; canonical security rules, AI-rule mirrors and product docs refreshed.
New /[locale]/privacy-choices page and footer link surfacing do-not-sell/share, targeted-advertising opt-out, GDPR, Swiss FADP, US state privacy, Canada PIPEDA and CASL rights
New My Account privacy/data-rights section with data export entry point, cookie consent management, regional rights summary, privacy policy access, privacy contact and deletion context
Privacy Choices cross-linked from the existing Terms, Privacy Policy and Legal Notice pages so users can move between all legal/privacy surfaces
/privacy-choices added to the sitemap and reserved CMS slug list so the route is discoverable and cannot be shadowed by dynamic CMS pages
Localized legal/footer/cookie/account copy added across en-US, en-CA, fr-FR and fr-CH
Cookie consent now honors browser Global Privacy Control signals — marketing/sharing/targeted-ad consent stays disabled even when 'Accept all' is clicked
Cookie settings UI surfaces the active GPC state and disables marketing consent while that signal is present
Canonical security rules updated to preserve /privacy-choices and Global Privacy Control behavior in future changes
Generated AI-rule mirrors synced for Codex, Gemini and OpenCode
Product docs updated for GDPR/privacy, cookie consent, analytics, My Account, production onboarding, README and the changelog
v1.25Minor
May 25, 2026
Marketing auth hydration, dashboard request deduplication, and docs
Public marketing pages stay anonymous-first and cacheable while authenticated UI hydrates client-side via /api/auth/me; private and org dashboards now share a request-cached context with aggregate SQL RPCs and composite indexes; canonical caching/auth rules and product docs refreshed.
Public marketing layout (app/[locale]/(frontend)/layout.tsx) no longer reads getUser() / Supabase session data server-side for navbar personalization — stays anonymous-first and cacheable
New GET /api/auth/me endpoint for client-side marketing-shell auth hydration: apiSecurity.public({ rateLimit: 'relaxed' }), server-side getUser(), minimal sanitized auth/profile/workspace payloads, and Cache-Control: private, no-store
New MarketingAuthProvider and MarketingCommandPalette; TransparentNavbar and AccountDropdown now consume hydrated auth context client-side — connected users clicking the anonymous login link are still routed to their dashboard by the existing auth-route guard
New core/accounts/dashboard-context.ts with React.cache() request-level helpers: getPrivateDashboardContext() for /private-dashboard/* and getOrgDashboardContext() for /org-dashboard/*
Private dashboard layout/pages (private-dashboard, chat, documents, referrals, affiliates) and org dashboard layout/pages (overview/admin, analytics, billing, API keys, members, roles, member detail, settings) now reuse shared auth/account/workspace context instead of repeating getUser(), profile, membership, billing-access and subscription reads
Protected dashboards remain dynamic and request-scoped; no user-specific data was moved to unstable_cache()
New dashboard aggregate RPCs for private overview counts, org analytics and org member usage summaries, plus composite indexes for ai_requests, chat_sessions and user_access_logs dashboard hot paths — org analytics/member pages now aggregate in SQL instead of pulling raw usage rows into Server Components
Credit purchase and end-trial client components lazy-loaded in dashboard sidebars/actions so modal/payment UI code is no longer part of the initial dashboard client bundle
Canonical caching rules updated to document marketing auth hydration, /api/auth/me rate-limit / Cache-Control expectations, and dashboard context deduplication
Product docs under content/docs refreshed for caching/performance, authentication, API reference and admin dashboards
v1.24Minor
May 24, 2026
Route-thinning, CI + Guardrails, Post-1.23 Security & Docs v2
Follow-on to 1.23: API route-thinning pass moves multi-step logic into core/*, a new GitHub Actions CI workflow with guardrail scripts and zero-warning lint gate, TRUST_CLOUDFLARE_IP / recent-auth / RLS-attribution security hardening, the multi-page public/docs/v2/ documentation rebuild, and refreshed AI-tool rules and entrypoints.
New TRUST_CLOUDFLARE_IP env gate so rate limiting no longer trusts spoofable CF-Connecting-IP — the header is only honored when explicitly enabled
Stripe webhook raw-body reads now enforce size while streaming the body instead of buffering the full arrayBuffer() first
/api/jobs/run cleanly separates the bearer-token cron path from the admin-session path with equivalent rate-limit, CSRF and body-size controls
Newsletter unsubscribe now targets only the authenticated user by default — no more anonymous arbitrary-email unsubscribes
Recent-auth + immutable admin_logs extended to subscription cancellation/reactivation, log purges, user credit grants and organization mutations
Org settings, invitation revocation, onboarding profile writes and /my-account profile updates moved behind validated server/API boundaries; account-scoped chat/document RLS tightened to block forged user_id attribution
API route-thinning pass: GDPR export, AI stream orchestration, CMS pages/media/blocks, admin settings/users/organizations/subscriptions and account API keys now delegate multi-step logic to core/gdpr, core/ai, core/cms, core/admin, core/organizations, core/billing and core/accounts — no app/api/**/route.ts remains over 250 lines
High-risk route splits completed: Stripe webhook dispatch in core/billing/stripe-webhook.ts, checkout session creation in core/billing/checkout.ts, job-run auth/execution in core/jobs/run-request.ts and bounded body / security-response helpers in lib/http/request-body.ts — routes are HTTP/security glue only
New .github/workflows/ci.yml runs on pull requests and pushes to main: typecheck, lint:ci, lint:strict, project guardrails, API-pattern coverage, API route size, RLS schema/policy coverage, Supabase type coverage, unit tests, secret scan, production build, accessibility and responsive checks
New/expanded guardrail scripts: project anti-patterns, API security-wrapper coverage, API route size, RLS coverage across schema.sql + cms-schema.sql, Supabase schema/type coverage, staging readiness probe and critical API load smoke test
lint:strict is now a zero-warning CI gate; historical warning visibility lives in npm run lint:debt (4,787 warnings measured during this pass)
New static multi-page documentation under public/docs/v2/ with shared navigation, responsive styling, grouped icon sidebar, per-page subsection menus, reused public/docs/screens/ assets, richer visual callouts and a global search-index.json covering setup, configuration, architecture, Supabase, auth/B2B, Stripe pricing, AI/RAG, operations, content, frontend, security, deployment, API reference and AI-tooling
public/docs/index.html reworked for first-project onboarding: clearer quick-start path, new-project configuration checklist, scenario-based environment guidance, explicit config/pricing.ts / Stripe Price ID setup, B2B checkout-before-onboarding and workspace auto-bootstrap notes, updated AI Blueprint skill counts, and removed stale local asset references
Canonical AI rules and /docs now document the admin-dashboard / billing maintainability split (core/admin/*, focused core/billing/* modules, affiliate/referral reversal hooks in risk-events.ts), the TRUST_CLOUDFLARE_IP env contract (also added to init-generated .env.local), authenticated unsubscribe behavior, the hardened jobs API, recent-auth admin requirements, stricter RLS/profile-write boundaries, the GDPR/US/Canada/Switzerland privacy expansion (fr-CH locale, CHF pricing, Swiss legal copy, CMS legal seeds), and the zero-warning lint gate / lint:debt / RLS coverage / staging probe / load smoke env contracts — generated AI-rule mirrors refreshed via scripts/sync-ai-rules.sh
Added .impeccable.md as the project design-context handoff; new CI/guardrail scripts documented across AI-tool entrypoints, canonical rules, README.md and product testing docs
v1.23Minor
May 19, 2026
Consolidated Audit Remediation, Billing, Docs, Email & AI Skills
Operators: apply 4 new migrations to the live DB — planfix DB hardening, subscriptions index (outside-tx), deletion processor status, and CMS locale key normalization (fr/en → fr-FR/en-US); mirrored in supabase/schema.sql and supabase/cms-schema.sql
Analytics + live-chat are now privacy-by-default: GTM, gtag/Google Ads, Meta Pixel, X Pixel and Crisp only load/fire after the matching analytics/marketing consent; purchase/signup events guarded at the application layer
Logged-in cookie consent now goes through POST /api/user/consents with apiSecurity.authenticated() + CSRF + Zod + rate limiting; browser components no longer upsert user_consents directly
CMS WYSIWYG image uploads now send the double-submit CSRF token and use media-appropriate rate limits; RAG document uploads keep auth + CSRF + body-size enforcement on the interactive standard limit
planFix.md remediation: 63/64 findings fixed — fail-closed auth.role() guard on referral/affiliate SECURITY DEFINER RPCs, API-key hash reads revoked from anon/authenticated roles, app_settings UPDATE RLS gained matching WITH CHECK
Admin verification + escalation tightened: 60s recent-auth cookie, per-email OTP rate-limit bucket, is_disabled rejection in verify-OTP and magic-link callback, recent-auth gates for admin_email and toggle_admin, audit logs for destructive actions
GDPR data export expanded — profile, memberships, chat sessions/messages, AI requests, consents, payments, subscriptions, licenses, invitations, access logs, deletion requests, notification prefs, pending emails, document metadata and API-key metadata (no secrets or hashes)
process-account-deletions records processor status for Stripe, newsletter/email, Supabase Storage, DB cascade and Supabase Auth; failures redacted, increment retry_count, mark the request failed and block false completion; Storage document/avatar cleanup runs before row deletion
New legal env contract: LEGAL_COMPANY_NAME, LEGAL_ADDRESS, LEGAL_REGISTRATION_NUMBER — npm run init, .env.example, config/app.ts and /docs all wired up; example placeholders intentionally block public/indexable production builds
npm run init now ends with a Pricing & Stripe guide — config/pricing.ts as the source of truth, price-ID mapping, billing model selection, webhook setup and payment methods
Ending a Stripe trial early synchronizes the local subscription mirror before responding (org-dashboard removes trial banners immediately); trial conversion credits subtract the configured trial-period grant from the plan allotment instead of adding the full plan amount on top
License-expiration warning now uses the documented two-step owner lookup instead of the invalid profiles!owner_user_id embed — fixes the live zero-email regression
Contact/newsletter attack-pattern detection wired; unwired CSRF helper code removed; CSRF Origin/Referer checks use appConfig.url; HTML iframe allowlisting now happens inside DOMPurify
Pricing tables now render included credits, project limits and trial credits from config/pricing.ts / trial env values instead of duplicated static feature value translations
CMS/page/blog/changelog locale lookup falls back from configured locales to legacy language-only keys; admin writes normalize payloads back to BCP-47 keys; seeds, sitemap and npm run init now use configured locales instead of fr / en
AI-tool docs synced across .claude/.codex/.gemini/.opencode/.agents — 22 files per mirror; coding-agent entrypoints now include a task-routing matrix, reviewer/skill selection and change-type verification pointers; scripts/sync-ai-rules.sh regenerates .agents/skills/ from .claude/skills/
Thin-route + core/ extractions cover AI streaming/accounting, changelog, chat mutations, billing subscribe-free, admin orgs/users and CMS validation; dead core/usage/ and core/memberships/ removed
CURRENCY_CODES is now the canonical currency tuple (Currency and SUPPORTED_CURRENCIES derive from it); agent model ids, referral salt, workspace signup bonus, RAG threshold, low-credit alert, locale/currency fallbacks and org-manager roles now use config helpers
Transactional email builders share lib/email/layout.ts: monochrome shell, table-based layout, dark-mode CSS, bulletproof CTA fallback and escaped text helpers; per-template gradients removed; covered by __tests__/email/templates-render.test.ts
/api/ai/stream body-size enforcement reads the cloned body as a stream instead of buffering the full arrayBuffer() first
RAG processing, Stripe sync, GDPR deletion processors, export reads, role reorders, affiliate revalidation and log purges parallelized or batched; select('*') replaced by explicit projections across the flagged hot paths
Next.js upgraded to 16.2.6; npm run audit:bundle now runs next build --webpack because @next/bundle-analyzer does not emit reports under Turbopack (reports in .next/analyze/)
/ui-ux-pro-max refreshed to v2.5.0: new data files (app-interface.csv, google-fonts.csv, design.csv, draft.csv), rewritten SKILL.md, web-stack lookup consolidated via --domain ux / --domain style; --stack nextjs / vue / shadcn deprecated
v1.22Minor
May 17, 2026
IPv6 Rate-Limit Fix & X Pixel
IPv6 rate-limit keys collapsed to the /64 prefix (bypass closed), a consent-gated X (Twitter) Pixel, and a /docs correctness + responsiveness pass.
IPv6 rate-limit bypass closed — keys collapsed to the /64 network prefix (RFC 4291) at a single chokepoint in createRateLimiter(); IPv4 unaffected
normalizeRateLimitIdentifier() never throws and leaves IPv4-mapped forms intact; getClientIP() still returns the full /128 for contact and IP-hash paths
New unit test __tests__/security/rate-limit-ip.test.ts: ::-expansion, prefix-collapse, IPv4 pass-through and malformed input
X (Twitter) Pixel via components/analytics/x-pixel.tsx — rendered only when consent.marketing is granted, same gate as the Meta Pixel
xEvents.purchase / xEvents.signUp fire from the existing trackPurchase / trackSignUp paths; no-op when conversion-event env vars are unset
Three optional NEXT_PUBLIC_X_PIXEL_* env vars, Zod-validated; static.ads-twitter.com added to the CSP allowlist
Standalone /docs page: removed two stale version sections, added a Changelog link to the single source of truth
Docs mobile table clipping fixed (13 tables wrapped), anchor scroll-offset, image lazy-loading, de-inlined a repeated pre style
v1.21Patch
May 14, 2026
Stripe Checkout Payment Methods
PayPal explicitly enabled for one-time payments via a single config source; subscriptions stay card-only (Stripe geo-restriction).
PayPal now appears on Stripe Checkout for one-time payments — payment_method_types explicitly requested instead of relying on Stripe auto-selection
New pricingConfig.checkoutPaymentMethods in config/pricing.ts, keyed per Stripe mode (subscription: card, payment: card + paypal)
CheckoutPaymentMethod literal union in lib/pricing/types.ts — a typo fails npm run build before deploy
Subscriptions stay card-only by default: Stripe restricts PayPal subscriptions to EEA/UK/CH merchants
Card-based wallets (Apple Pay / Google Pay) unaffected — they ride on the card entry
npm run init reminds operators to activate the methods in Stripe Dashboard for both test and live
Docs updated: .claude/rules/billing.md and mirrors, domain-map config-source registry
v1.20Minor
May 11, 2026
Magic-Link UX Overhaul
6-digit code fallback, cross-tab auto-redirect, resend cooldown and copy refresh to cut magic-link drop-off.
6-digit OTP code surfaced in the magic-link email alongside the link; length configurable via OTP_LENGTH env (default 6, clamped 6-10)
New endpoint POST /api/auth/verify-otp — Zod-validated, apiSecurity.public() + strict rate limit, generic error (no enumeration)
Success screen shows the destination email, numeric OTP input (autoComplete one-time-code) and a Wrong email? back button
Cross-tab auto-redirect: success screen polls getUser() every 4s (paused when tab hidden); clicking the link in another tab redirects the original
Resend cooldown (30s countdown) and spam-folder reminder
Copy refresh: Continue with email, subtitle mentions link + code, email subject Your sign-in code
9 new auth.login.* and 3 email.magicLink.* keys in both locales (parity test green)
Init script prompts for OTP_LENGTH (validated); operator caveat: it must match the Supabase project Email OTP length setting
v1.19Minor
May 10, 2026
Prelaunch Sign-In Gate & Dynamic OAuth Catalog
Server-side admin allowlist gating sign-in during prelaunch, plus a configurable multi-provider OAuth button catalog.
Prelaunch sign-in now gated server-side by PRELAUNCH_ALLOWED_EMAILS (server-only, comma-separated, case-insensitive; empty = locked)
Three enforcement points: magic-link 403 before generateLink, callback sign-out, OAuth sign-out + admin.deleteUser cleanup
Generic restriction message — no email-enumeration vector
Dynamic OAuth catalog: NEXT_PUBLIC_OAUTH_PROVIDERS renders one branded button per id, validated against all 19 Supabase-supported providers
New lib/auth/oauth-providers.tsx (inlined brand SVGs, no new dependency) + components/auth/oauth-buttons.tsx
login-form.tsx: hardcoded Google button removed; divider only shows when at least one provider is configured
i18n: auth.login.continueWithProvider with {provider} placeholder + auth.login.prelaunchRestricted, both locales (parity green)
Init script hydrates both env vars on re-runs; public/docs/index.html OAuth + prelaunch sections rewritten
v1.18Minor
May 4, 2026
Affiliation Program & Provider-Agnostic Email
Account-centric partner program with cash commissions (Stripe Connect deferred) and a pluggable email abstraction supporting Brevo, Mailjet and noop.
AFFILIATES_ENABLED feature gate returns 404 everywhere when off — surface invisible until you flip it
Provider-agnostic email abstraction (EmailProvider interface + factory + adapters); Brevo, Mailjet and noop ship in the box
EMAIL_PROVIDER env var with auto-fallback to noop when credentials missing (dev/CI friendly)
Newsletter forms (waitlist, landing-newsletter, newsletter-form) now actually call /api/newsletter/subscribe with Turnstile token — pre-existing 400 bug fixed
v1.17Patch
April 29, 2026
pg_cron Headers Fix & Cron Admin Dashboard
Critical pg_net positional-args fix (secret leak + 401 storm) plus a new admin surface for monitoring and managing cron.job entries.
CRITICAL: pg_net positional-args fix — JOBS_SECRET_KEY was being sent as URL query string, leaking into access logs every cron tick. Rotate the secret after upgrading.
Cron jobs were silently 401'ing — net.http_post now called with named arguments, robust across pg_net versions
Cron Admin Dashboard at /admin-dashboard/jobs/cron — list, toggle, unschedule, purge-orphans
Bearer tokens in cron.job.command redacted at the SQL layer — never cross the API/UI boundary
Recent-auth gate on destructive cron actions (unschedule, purge-orphans); audit trail on every mutation