Site

Tech Stack

Category Technology
Framework Next.js 16 (App Router), React 19, TypeScript
Database Shared Prisma + PostgreSQL on Supabase or Neon, with common Account-scoped SQL/RLS and explicit provider overlays.
Auth Supabase Auth or Better Auth, selected once per deployment — provider-neutral Magic Link + OTP flows, canonical application-identity finalization, cross-tab session observation, and config-driven OAuth.
Payments Stripe, Lemon Squeezy, or Paddle selected once per installation; provider-normalized subscriptions and one-time purchases.
AI LangChain + Multi-LLM (OpenAI, Anthropic, Google)
Email Provider-agnostic abstraction — Brevo or Mailjet (transactional + newsletter), pluggable via EMAIL_PROVIDER
Styling Tailwind CSS 4, shadcn/ui (New York style), hue-parameterised oklch tokens — rotate --brand-h in app/globals.css to rebrand the whole product
Fonts DM Sans (body) + JetBrains Mono (numerals, code, logs), self-hosted via next/font/google
Animation Motion (motion) mounted through LazyMotion on the marketing surface only; dashboards use CSS transitions on the duration/easing tokens
i18n URL-based routing (/fr-FR/..., /en-US/..., /en-CA/..., /fr-CH/...)
Security CSRF, rate limiting (Upstash Redis), CSP headers, recent-auth gates for destructive admin actions
Bot Protection Cloudflare Turnstile (invisible CAPTCHA)
Analytics Google Tag Manager, Google Ads, Meta Pixel, X Pixel (all consent-gated)
Live Chat Crisp (consent-gated user identification)
Caching Upstash Redis (rate limiting, sessions)
Background Jobs Native Supabase/Neon pg_cron or external scheduler + application workers

Third-Party Integrations

The boilerplate includes pre-configured integrations for common SaaS needs:

Payment adapters - Stripe, Lemon Squeezy, or Paddle subscriptions, one-time payments & billing
Database, Auth & Storage adapters - Supabase or Neon PostgreSQL; Supabase Auth or Better Auth; Supabase Storage, Neon Object Storage, AWS S3, or disabled
LangChain - Multi-LLM AI orchestration
Email (Brevo / Mailjet) - Transactional emails & newsletter via pluggable provider
Cloudflare Turnstile - Bot protection for forms
Google Tag Manager - Analytics & tracking
Google Ads - Per-action conversion tracking (purchase & signup) via gtag.js
Meta Pixel - Facebook/Instagram ads tracking
Crisp - Live chat widget
Upstash Redis - Rate limiting & caching
Serwist - PWA, offline support & push notifications

Payment providers

Billing has provider-neutral contracts and adapter integration for Stripe, Lemon Squeezy, and Paddle. The initialization script selects exactly one provider in server-only PAYMENTS_PROVIDER; customers never choose it and an existing installation cannot switch in place. All three providers implement member checkout and customer portal locally. Paddle uses an approved local Paddle.js page and keeps a durably prelinked member transaction payable without treating the browser-presentation TTL as a financial expiry; its anonymous guest surface stays closed because it lacks that Account-owned prelink. Shared domain effects own payments, credits, entitlements, holds, and reconciliation. Plans and provider-, currency-, interval-, and mode-scoped catalogue bindings are defined in config/pricing.ts, not the database. Stripe acts as payment processor for the application seller; Lemon Squeezy and Paddle are Merchant of Record.

Database, Auth and Storage providers

DATABASE_PROVIDER selects Supabase PostgreSQL or Neon for the shared Prisma business runtime; AUTH_PROVIDER independently selects Supabase Auth or Better Auth; STORAGE_PROVIDER independently selects disabled, Supabase Storage, Neon Object Storage, or AWS S3. Product domains resolve a provider principal into private.app_users and then authorize only with the resulting application actor and Account memberships. Object access stays behind server-only adapters: private documents use signed URLs and public CMS media uses a configured bucket/CDN base URL. Provider-native clients never become a business-database portability escape hatch. Background work runs through the durable shared job registry; a Supabase Edge Function may invoke it but does not own business persistence.

Adding your own account-scoped table

Every domain table carries account_id (never a bare user_id) and is filtered by membership. Replicate this pattern for any new core/app table, and update its owning source under database/ in the same commit. Run pnpm run db:schema:check to validate assembly. The source and manifest are the sole DDL authority; init and guarded local resets install complete SQL batches in memory. Optional exports are only for manual installation and are never edited or committed:

alter table my_table enable row level security;

create policy "members_read" on my_table for select
using (exists (
  select 1 from memberships m
  where m.account_id = my_table.account_id
    and m.user_id = private.current_actor_id()
));

-- INSERT/UPDATE/DELETE must be at least as strict as SELECT
create policy "members_write" on my_table for all
using (exists (
  select 1 from memberships m
  where m.account_id = my_table.account_id
    and m.user_id = private.current_actor_id()
));

If a policy needs to query memberships recursively, use a SECURITY DEFINER helper (e.g. user_belongs_to_account()) instead of an inline sub-select to avoid RLS recursion.

AI / Multi-LLM

AI is orchestrated via LangChain behind one server-only LlmClient. AI_LLM_TRANSPORT selects direct OpenAI/Anthropic/Google APIs or OpenRouter for chat and embeddings together; gateway requests enforce ZDR/data-collection-deny routing. A dynamic registry provides chat, code-assistant, translator, writer, RAG, and custom agents without exposing provider SDKs to agent code. Streaming SSE, abort/time ceilings, provider-reported token accounting, bounded embeddings, prompt caching, and model-provider/transport audit persistence are normalized at the boundary.

Cloudflare Turnstile

Turnstile provides invisible bot protection without annoying CAPTCHAs. It's used on contact forms, login, and other sensitive endpoints.

Google Tag Manager, Google Ads, Meta Pixel & X Pixel

Analytics are consent-aware and privacy-by-default. GTM, gtag.js / Google Ads, Meta Pixel, X (Twitter) Pixel, and purchase/signup conversion events only load or fire after the user grants the matching analytics or marketing consent. Consent Mode v2 still starts deny-by-default, but third-party scripts are not mounted before consent. To enable Google Tag Manager, set NEXT_PUBLIC_GTM_ID. For Google Ads conversion tracking, set NEXT_PUBLIC_GOOGLE_ADS_ID and NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_LABEL. Optionally set NEXT_PUBLIC_GA_MEASUREMENT_ID for GA4 via gtag.js (skip if GA4 is already configured in your GTM container to avoid duplicate events). For Meta Pixel, set NEXT_PUBLIC_META_PIXEL_ID. For the X Pixel, set NEXT_PUBLIC_X_PIXEL_ID plus the optional per-action conversion event IDs NEXT_PUBLIC_X_PIXEL_EVENT_PURCHASE / NEXT_PUBLIC_X_PIXEL_EVENT_SIGNUP. A unified trackConversion() method fires purchase events only when marketing consent is granted.

Crisp Live Chat

Crisp provides live chat with automatic user identification for logged-in users. Set the NEXT_PUBLIC_CRISP_WEBSITE_ID variable to enable it. The script and user-identification payload are gated by consent, so Crisp is not loaded before the user grants the configured support/marketing consent category.

Email (Brevo / Mailjet)

Transactional emails (magic links, workspace invitations, contact-form confirmations, license expiration warnings, organization deletion notices) and newsletter management go through a provider-agnostic abstraction in lib/email/. Switching providers is a config-only change — set EMAIL_PROVIDER and supply the matching credentials.

Supported providers:

  • brevo — Brevo (formerly Sendinblue). Single API key (BREVO_API_KEY) + BREVO_NEWSLETTER_LIST_ID.
  • mailjet — Mailjet Send API v3.1 + Contacts. Two keys (MAILJET_API_KEY_PUBLIC + MAILJET_API_KEY_PRIVATE) + optional MAILJET_NEWSLETTER_LIST_ID.
  • noop — logs and reports success without delivering. Useful for dev / CI.

The shared sender identity (EMAIL_FROM_NAME, EMAIL_FROM_ADDRESS) and inbox addresses (CONTACT_EMAIL, SUPPORT_EMAIL) apply to every provider. Every transactional template is localized for the configured locale catalogue and composes the mobile-first design-system shell in lib/email/layout.ts: fluid from 320px to a 600px cap, dark-mode aware, protected from client text amplification, and backed by an Outlook-compatible CTA. Builders live in lib/email/templates.ts; lib/email/translations.ts holds the strings. Failed transactional sends are queued in the pending_emails table and retried with exponential backoff by the process-pending-emails job.

Adding a new provider is a 3-step change: implement the EmailProvider interface under lib/email/providers/<name>.ts, add a credentials slice in config/email.ts, and add a branch in lib/email/provider.ts. No call sites need to change.

Upstash Redis

Upstash Redis provides distributed rate limiting across all API endpoints and shared LLM circuit-breaker state across workers. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN to enable it. Both are required in production — the app refuses to boot without them. Process-local state is per-instance, so behind a load balancer it does not merely fail to share rate limits, it leaves every endpoint effectively unlimited and lets LLM circuit decisions disagree. Local state remains the development default, and a genuinely single-instance production deployment can opt in with ALLOW_INMEMORY_RATE_LIMIT=true.

Serwist (PWA)

Serwist powers the Progressive Web App layer with service worker generation, offline support, and push notifications. It provides automatic precaching of static assets, stale-while-revalidate for resources, network-first for API calls, and an offline fallback page. Push notifications use the Web Push API with VAPID authentication, per-device subscriptions stored in the database, and user notification preferences. Configurator mode keeps Next.js on Turbopack, then @serwist/cli bundles the worker from serwist.config.mjs; SerwistProvider handles browser registration.

Account-Centric Model

Everything is attached to an Account, never directly to a User. This enables multi-tenant architecture with clear separation of resources.

PROVIDER PRINCIPAL (provider + issuer + subject) │ │ resolves through auth_identities ▼ APP USER (private.app_users) │ │ can have multiple ▼ MEMBERSHIPS (user_id, account_id, role_slug) │ │ belongs to ▼ ACCOUNT (personal | workspace) ├── Credits Balance ├── Subscription (selected payment provider) ├── AI Usage Logs ├── Chat Sessions ├── API Keys (B2B) └── Settings