nextjsboilerplate Docs
Guides

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 :

PlaceholderWhere to change it
Brand name (display)EMAIL_FROM_NAME env var + src/utils/AppConfig.ts → AppConfig.name
Sender addressEMAIL_FROM env var
Logo (header + footer + emails)public/assets/logo.svg + src/components/landing/Logo.tsx
Favicon + app iconssrc/app/icon.png, src/app/apple-icon.png
OG imagesrc/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 entriessrc/app/sitemap.ts
security.txt contactSECURITY_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

  1. 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.
  2. (Optional) Override --color-secondary* if your brand has a second colour.
  3. Override --font-sans if 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.json

Translate 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 confirm

Server-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

  1. Create src/libs/billing/polar.ts exporting a polarProvider: BillingProvider.
  2. Implement createCheckoutSession, createPortalSession, verifyWebhook, fetchSubscription against the Polar SDK.
  3. Register the adapter in PROVIDERS in src/libs/Billing.ts.
  4. Set BILLING_PROVIDER=polar in production, set POLAR_API_KEY and POLAR_WEBHOOK_SECRET env vars.
  5. Update the webhook route at /api/billing/webhook to call Billing.verifyWebhook() instead of the Stripe-specific function. The route handler is already provider-agnostic in the upstream — see src/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-name

When 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) :

  1. Find the rollback section for the feature in CLAUDE.md.
  2. Drop the listed migration(s).
  3. Delete the listed src/libs/*, src/actions/*, route, and component files.
  4. Unwire any cron entries from src/jobs/registry.ts.
  5. 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.md at the repo root — the canonical reference for every feature, with rollback paths.

On this page