Customizing the boilerplate
Design tokens (Tailwind v4), adding a locale to next-intl, swapping the payment provider (Stripe → Polar / Paddle), and the branding placeholders to swap on day one.
Customizing the boilerplate
This boilerplate is meant to be forked, branded, and shipped — not read-only. Every customisation point below has a single canonical file to edit, and the rest of the codebase reads from it.
1. Branding placeholders to swap on day one
The codebase ships with neutral placeholders. Search-and-replace these on your first commit :
| Placeholder | Where to change it |
|---|---|
| Brand name (display) | EMAIL_FROM_NAME env var + src/utils/AppConfig.ts → AppConfig.name |
| Sender address | EMAIL_FROM env var |
| Logo (header + footer + emails) | public/assets/logo.svg + src/components/landing/Logo.tsx |
| Favicon + app icons | src/app/icon.png, src/app/apple-icon.png |
| OG image | src/app/opengraph-image.png (1200x630) |
| Marketing copy (hero, features, pricing) | src/locales/{en,fr}.json namespaces Hero, Features, Pricing |
| Footer links (Privacy, ToS, Security) | src/components/landing/Footer.tsx + src/app/[locale]/(marketing)/legal/* |
| Sitemap entries | src/app/sitemap.ts |
security.txt contact | SECURITY_CONTACT_EMAIL + SECURITY_POLICY_URL env vars |
There is no BRAND_NAME env var on purpose — EMAIL_FROM_NAME already
serves that role and changing it in one place is enough for both email
headers and the dashboard sidebar brand line.
2. Design tokens — Tailwind v4 (CSS-first)
The boilerplate uses Tailwind CSS v4 with the new @theme syntax.
There is no tailwind.config.js color block — every token is a CSS
custom property declared inside @theme in src/styles/global.css.
Token layers
/* src/styles/global.css (excerpt) */
@theme {
/* Layer 1: primitive — raw values */
--color-primary: #3b82f6;
--color-primary-strong: #2563eb;
--color-primary-soft: #60a5fa;
--color-primary-foreground: #f8fafc;
--color-secondary: #1d4ed8;
--color-secondary-strong: #1e40af;
--color-secondary-soft: #93c5fd;
/* Layer 2: semantic — what the role does */
--color-hero-foreground: #f8fafc;
--color-hero-muted: rgba(226, 232, 240, 0.72);
--color-surface-card: rgb(15 23 42 / 0.92);
--color-surface-border: rgb(255 255 255 / 0.08);
--color-text-on-surface: rgb(248 250 252 / 0.97);
/* Typography */
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
}Re-skin to your brand in three steps
- Pick a primary brand colour. Override the four
--color-primary*tokens — Tailwind v4 will rebuild every utility class (bg-primary,text-primary, etc.) on next compile. - (Optional) Override
--color-secondary*if your brand has a second colour. - Override
--font-sansif you ship your own font.
That's it — every primitive Tailwind class now reflects your brand.
Components that use cva variants (Button, Badge) inherit automatically.
Dark mode
Dark mode is wired through next-themes. The override block lives under
:root.dark in the same global.css :
:root.dark {
--color-surface-card: rgb(10 14 24 / 0.96);
--color-text-on-surface: rgb(241 245 249 / 0.92);
/* ... */
}Override the same variables under :root.dark to re-skin dark mode.
3. Adding a locale (next-intl)
The boilerplate ships with en (default) and fr. Adding a third
locale is four steps :
Step 1 — Create the message catalogue
cp src/locales/en.json src/locales/es.jsonTranslate every value. Keys must remain identical — npm run check:i18n
fails the build if a key is missing or a value is left in English.
Step 2 — Register the locale
Open src/utils/AppConfig.ts :
export const AppConfig = {
name: 'Your SaaS',
locales: ['en', 'fr', 'es'], // ← add the new locale
defaultLocale: 'en',
localePrefix: 'as-needed',
};Step 3 — Update middleware.ts (if applicable)
The middleware reads from AppConfig.locales so no change is needed for
the routing itself. Update only the maintenance-mode allow-list if the
new locale needs a custom maintenance redirect path (it doesn't, by
default).
Step 4 — Verify
npm run check:i18n # all keys present in every locale
npm run check:types # TypeScript narrowing on locale unions
npm run dev # visit /es to confirmServer-side strings (emails, audit-chain)
Email templates and the audit-chain payload formatter both consume
locale-scoped strings via loadLocale(locale). They auto-discover the
new file — no further wiring needed.
4. Swapping the payment provider
Stripe is wired today. The boilerplate is provider-agnostic at the
boundary — the Billing interface in src/libs/Billing.ts is what
every consumer talks to. To swap to Polar or
Paddle, replace one file :
Strategy
// src/libs/Billing.ts (excerpt of the interface)
export interface BillingProvider {
createCheckoutSession(input: CheckoutInput): Promise<{ url: string }>;
createPortalSession(customerId: string): Promise<{ url: string }>;
verifyWebhook(rawBody: string, signature: string): WebhookEvent;
fetchSubscription(customerId: string): Promise<SubscriptionState>;
}The default export of Billing.ts is a switch on BILLING_PROVIDER :
const PROVIDERS: Record<string, BillingProvider> = {
stripe: stripeProvider,
polar: polarProvider, // adapter you write
paddle: paddleProvider, // adapter you write
};
export const Billing = PROVIDERS[process.env.BILLING_PROVIDER ?? 'stripe'];Add a Polar adapter
- Create
src/libs/billing/polar.tsexporting apolarProvider: BillingProvider. - Implement
createCheckoutSession,createPortalSession,verifyWebhook,fetchSubscriptionagainst the Polar SDK. - Register the adapter in
PROVIDERSinsrc/libs/Billing.ts. - Set
BILLING_PROVIDER=polarin production, setPOLAR_API_KEYandPOLAR_WEBHOOK_SECRETenv vars. - Update the webhook route at
/api/billing/webhookto callBilling.verifyWebhook()instead of the Stripe-specific function. The route handler is already provider-agnostic in the upstream — seesrc/app/api/billing/webhook/route.ts.
The audit_chain event names (subscription.upgraded, etc.) are
provider-neutral on purpose, so the rest of the codebase doesn't change
when you swap providers.
What stays Stripe-specific
The CLI tooling (stripe listen, stripe trigger) is obviously
Stripe-specific. If you swap providers, your local-dev event-replay
loop will use the equivalent Polar / Paddle CLIs.
5. Swapping the email provider
Same pattern as billing. The dispatch() helper inside src/libs/Email.ts
is the only place that talks to Resend :
async function dispatch({ to, subject, html }: DispatchInput): Promise<EmailResult> {
if (!process.env.RESEND_API_KEY) {
console.warn(`[email-stub] to=${to} subject=${subject}\n${html}`);
return { success: true };
}
const result = await resend.emails.send({
from: senderHeader(),
to,
subject,
html,
});
return { success: !result.error, id: result.data?.id, error: result.error?.message };
}To swap to Postmark, AWS SES, or any other provider, rewrite dispatch()
— every Email.send* method calls it. The validation, locale
resolution, and per-category preference checks all stay unchanged.
6. Swapping the LLM provider
Already supported out of the box. Append the value to llmProviderEnum
in src/models/Schema.ts, add a ProviderStrategy to STRATEGIES in
src/libs/Llm.ts, generate a Drizzle migration. See CLAUDE.md →
"How to add a new provider" for the full checklist (six steps).
7. Swapping the storage backend
src/libs/Storage.ts is a thin wrapper around an S3-compatible client.
Set the four STORAGE_* env vars to point at any S3-API provider :
STORAGE_PROVIDER=s3 # or 'r2', 'minio', 'b2'
STORAGE_ENDPOINT=https://your-bucket.example
STORAGE_ACCESS_KEY=...
STORAGE_SECRET_KEY=...
STORAGE_BUCKET=your-bucket-nameWhen STORAGE_* is unset, the library falls back to the local filesystem
under local-storage/ — local dev works without any cloud account.
8. Removing features you don't need
The boilerplate is opinionated but every primitive has a documented
rollback path in CLAUDE.md. To remove a feature (e.g. you don't need
the LLM admin surface) :
- Find the rollback section for the feature in
CLAUDE.md. - Drop the listed migration(s).
- Delete the listed
src/libs/*,src/actions/*, route, and component files. - Unwire any cron entries from
src/jobs/registry.ts. - Run
npm run check:types— the compiler tells you if any consumer was left dangling.
The features are deliberately decoupled so removing one never cascades into rewriting another.
9. Renaming the project
# 1. Rename the directory + repo
git remote set-url origin git@github.com:your-org/your-name.git
# 2. Update package.json
# - "name": "your-name"
# - "description": "Your description"
# 3. Update src/utils/AppConfig.ts
# - AppConfig.name
# 4. Update README.md heading
# 5. Search-and-replace any leftover references
grep -r "Next-js-Boilerplate\|nextjsboilerplate" --include="*.ts" --include="*.tsx" --include="*.md"What's next
- Initial setup — re-run the bootstrap checklist after a major customisation pass.
- Compliance moat — verify your audit chain still validates after schema changes.
CLAUDE.mdat the repo root — the canonical reference for every feature, with rollback paths.
Compliance moat
Audit chain (WORM), RGPD export & deletion, idempotent webhooks, CSP. The compliance primitives that turn a SaaS demo into a SaaS your CFO will sign off on — with code examples.
Architecture overview
How auth, billing, the database, jobs, and the compliance primitives fit together — and where the seams are.