Customization

Step-by-step guide to make this starter your own.

# Branding

App Name

Change in .env. This propagates everywhere: metadata, emails, landing page header/footer.

NEXT_PUBLIC_APP_NAME="Your App Name"

Colors

Edit src/styles/globals.css -- the @theme section:

@theme {
  --color-primary-500: #your-color;
  --color-primary-600: #your-darker;
  /* ... all shades from 50 to 950 */
}

Replace the text-based logo in these files:

  • -- src/components/landing/header.tsx -- navigation logo
  • -- src/components/landing/footer.tsx -- footer logo
  • -- src/components/dashboard/sidebar.tsx -- dashboard logo

Favicon and OG Image

Replace files in public/: public/favicon.ico and public/og.png (1200x630px recommended).

# Plans and Pricing

Edit Plans

In src/lib/stripe.ts, modify the PLANS object. This is the single source of truth for plan names, prices, limits, and features:

export const PLANS = {
  free: {
    name: "Free",
    price: { monthly: 0, yearly: 0 },
    limits: { projects: 3, apiCalls: 100 },
    features: ["3 projects", "100 API calls/month"],
  },
  pro: {
    name: "Growth",
    price: { monthly: 49, yearly: 490 },
    limits: { projects: 25, apiCalls: 50000 },
    features: ["25 projects", "50K API calls/month", "Priority support"],
  },
  // ...
};

Stripe Products

  1. Create matching products in Stripe Dashboard
  2. Create monthly + yearly prices for each paid plan
  3. Update .env with new price IDs
STRIPE_PRICE_PRO_MONTHLY=price_xxx
STRIPE_PRICE_PRO_YEARLY=price_xxx

Access Plan Limits in Code

import { PLANS } from "@/lib/stripe";

const userPlan = PLANS[user.plan.toLowerCase()];
if (userPlan.limits.projects !== -1 && projectCount >= userPlan.limits.projects) {
  // Show upgrade prompt
}

# Landing Page

Section File What to edit
Herolanding/hero.tsxHeadline, subheading, CTA buttons
Featureslanding/features.tsxFeatures array (icon, title, description)
Pricinglanding/pricing.tsxAuto-generated from PLANS config
FAQlanding/faq.tsxQuestions/answers array
Headerlanding/header.tsxNav links
Footerlanding/footer.tsxColumns, links, social links
Tip: Icons come from lucide-react. Browse the full icon set at lucide.dev/icons.

# Dashboard

Add Sidebar Items

Edit src/components/dashboard/sidebar.tsx:

const navItems = [
  { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
  { name: "Projects", href: "/dashboard/projects", icon: FolderOpen }, // NEW
  { name: "Settings", href: "/dashboard/settings", icon: Settings },
  { name: "Billing", href: "/dashboard/billing", icon: CreditCard },
];

Add New Dashboard Page

Create src/app/dashboard/projects/page.tsx:

import { requireUser } from "@/lib/auth";
import { DashboardHeader } from "@/components/dashboard/dashboard-header";

export default async function ProjectsPage() {
  const user = await requireUser();
  return (
    <div>
      <DashboardHeader title="Projects" description="Manage your projects" />
      {/* Your content */}
    </div>
  );
}

# Adding a New Feature (Example: Projects)

Step 1: Database Model

Add to prisma/schema.prisma:

model Project {
  id          String   @id @default(cuid())
  name        String
  description String?
  userId      String
  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@map("projects")
}

Add the relation to User model, then run:

npx prisma db push

Step 2: Create Page

Create src/app/dashboard/projects/page.tsx (see dashboard section above).

Step 3: Add API Routes

Create src/app/api/projects/route.ts:

import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET() {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const projects = await prisma.project.findMany({ where: { userId } });
  return NextResponse.json(projects);
}

export async function POST(request: Request) {
  const { userId } = await auth();
  if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const { name, description } = await request.json();
  const project = await prisma.project.create({
    data: { name, description, userId },
  });
  return NextResponse.json(project);
}

Step 4: Add Sidebar Nav Item

Edit sidebar.tsx to include the new Projects link (see dashboard section above).

Step 5: Enforce Plan Limits

import { PLANS } from "@/lib/stripe";

const user = await requireUser();
const plan = PLANS[user.plan.toLowerCase() as keyof typeof PLANS];
const projectCount = await prisma.project.count({ where: { userId: user.id } });

if (plan.limits.projects !== -1 && projectCount >= plan.limits.projects) {
  // Return upgrade prompt
}

# Blog Posts

Create content/blog/your-post.mdx:

---
title: "Your Post Title"
description: "Brief description for SEO"
date: "2025-03-20"
author: "Your Name"
tags: ["saas", "tutorial"]
published: true
---

# Your Content

Write in standard Markdown/MDX.

It auto-appears on /blog. No configuration needed.

# Email Templates

Add new email functions in src/lib/email.ts:

export async function sendCustomEmail(email: string, data: any) {
  const FROM = process.env.RESEND_FROM_EMAIL || "noreply@example.com";
  const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "SaaS Starter";
  return getResend().emails.send({
    from: `${APP_NAME} <${FROM}>`,
    to: email,
    subject: "Your subject",
    html: `<h1>Your HTML email</h1>`,
  });
}

Preview email templates locally with npm run email:dev.

# Adding API Routes

Create a new route at src/app/api/your-route/route.ts:

import { auth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";

export async function GET() {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Your logic here
  return NextResponse.json({ data: "..." });
}

For public routes, skip the auth check but consider adding rate limiting:

import { rateLimit } from "@/lib/rate-limit";

export async function POST(request: Request) {
  const ip = request.headers.get("x-forwarded-for") || "unknown";
  const { success } = rateLimit(ip, 10, 60000);
  if (!success) {
    return NextResponse.json({ error: "Too many requests" }, { status: 429 });
  }
  // ...
}

# Internationalization (i18n)

To add multi-language support:

  1. Install next-intl: npm install next-intl
  2. Create messages/fr.json and messages/en.json
  3. Wrap app with IntlProvider
  4. Replace hardcoded strings with useTranslations()

See the next-intl documentation for a full guide.