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 */
}
Logo
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
- Create matching products in Stripe Dashboard
- Create monthly + yearly prices for each paid plan
- Update
.envwith 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 |
|---|---|---|
| Hero | landing/hero.tsx | Headline, subheading, CTA buttons |
| Features | landing/features.tsx | Features array (icon, title, description) |
| Pricing | landing/pricing.tsx | Auto-generated from PLANS config |
| FAQ | landing/faq.tsx | Questions/answers array |
| Header | landing/header.tsx | Nav links |
| Footer | landing/footer.tsx | Columns, links, social links |
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:
- Install
next-intl:npm install next-intl - Create
messages/fr.jsonandmessages/en.json - Wrap app with IntlProvider
- Replace hardcoded strings with
useTranslations()
See the next-intl documentation for a full guide.