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