Customization Guide
How to customize and extend Unstack Pro for your product
Customization Guide
Customize Unstack Pro to match your brand and product needs. The template is designed to be extended while keeping the core auth and organization features intact.
You don't need to modify auth code to customize your app. Focus on branding, adding features, and extending the schema.
Branding
Site Configuration
Update your app's name, description, and metadata in /config/site.ts:
export const siteConfig = {
name: "Your App Name",
description: "Your app description for SEO and social sharing",
url: "https://yourdomain.com",
ogImage: "https://yourdomain.com/og.png",
links: {
twitter: "https://twitter.com/yourapp",
github: "https://github.com/yourorg/yourapp",
},
};Logo and Favicon
Replace the default logos:
- Favicon: Replace
/app/favicon.ico - Logo: Add your logo to
/public/logo.pngand/public/logo-dark.png - Update components that reference the logo
import Image from "next/image";
export function Logo() {
return (
<>
<Image
src="/logo.png"
alt="Your App"
width={120}
height={40}
className="dark:hidden"
/>
<Image
src="/logo-dark.png"
alt="Your App"
width={120}
height={40}
className="hidden dark:block"
/>
</>
);
}Colors and Theme
Unstack Pro uses Tailwind CSS v4 with CSS variables for theming. Customize colors in /styles/globals.css:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}Fonts
Customize fonts in /config/fonts.ts:
import { Inter, JetBrains_Mono } from "next/font/google";
export const fontSans = Inter({
subsets: ["latin"],
variable: "--font-sans",
});
export const fontMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
});Adding OAuth Providers
Better Auth supports many OAuth providers. To add one:
Configure the Provider
Add a socialProviders block to createAuthOptions in
convex/betterAuth/auth.ts — this is the source of truth for auth config (Better
Auth runs inside Convex):
// inside createAuthOptions(...)
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},No social providers are configured out of the box — the stack is OAuth-ready.
Because these env vars are read in the Convex runtime, set them with
bunx convex env set (see below).
Add Environment Variables
Add OAuth credentials to your environment:
# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# GitHub OAuth
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"Don't forget to also set these in Convex:
bunx convex env set GOOGLE_CLIENT_ID "your-google-client-id"
bunx convex env set GOOGLE_CLIENT_SECRET "your-google-client-secret"Add Sign-In Button
Add the OAuth button to your login page:
"use client";
import { authClient } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
import { FaGoogle, FaGithub } from "react-icons/fa";
export function OAuthButtons() {
const signInWithGoogle = async () => {
await authClient.signIn.social({
provider: "google",
callbackURL: "/",
});
};
const signInWithGithub = async () => {
await authClient.signIn.social({
provider: "github",
callbackURL: "/",
});
};
return (
<div className="grid gap-2">
<Button variant="outline" onClick={signInWithGoogle}>
<FaGoogle className="mr-2 h-4 w-4" />
Continue with Google
</Button>
<Button variant="outline" onClick={signInWithGithub}>
<FaGithub className="mr-2 h-4 w-4" />
Continue with GitHub
</Button>
</div>
);
}Configure OAuth App
Set up the OAuth app with the provider:
Google:
- Go to Google Cloud Console
- Create OAuth 2.0 credentials
- Add authorized redirect URI:
https://yourdomain.com/api/auth/callback/google
GitHub:
- Go to GitHub Developer Settings
- Create new OAuth App
- Set callback URL:
https://yourdomain.com/api/auth/callback/github
For a full list of supported providers, see the Better Auth Social Providers documentation.
Adding New Pages
Create a New Route
Add pages using Next.js App Router conventions:
import { requireSession } from "@/lib/session";
export default async function DashboardPage() {
const session = await requireSession();
return (
<div className="container py-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
<p>Welcome, {session.user.name}!</p>
</div>
);
}Protected Routes
Use requireSession() to protect routes - it automatically redirects to the login page if the user is not authenticated:
import { requireSession } from "@/lib/session";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
await requireSession();
return <>{children}</>;
}Organization-Scoped Routes
Access organization context in routes:
import { requireSession } from "@/lib/session";
interface Props {
params: { organizationSlug: string };
}
export default async function ProjectsPage({ params }: Props) {
const session = await requireSession();
const { organizationSlug } = params;
// Fetch organization-specific data
// Verify user has access to this organization
return (
<div>
<h1>Projects for {organizationSlug}</h1>
{/* Your content */}
</div>
);
}Customizing Auth Flows
Custom Email Templates
Modify email templates in /emails/:
import { Body, Container, Heading, Html, Text } from "@react-email/components";
export default function VerificationEmail({ otp }: { otp: string }) {
return (
<Html>
<Body style={{ fontFamily: "sans-serif" }}>
<Container>
<Heading>Welcome to Your App!</Heading>
<Text>Your verification code is: <strong>{otp}</strong></Text>
<Text>This code expires in 10 minutes.</Text>
</Container>
</Body>
</Html>
);
}Custom Validation Rules
Extend Zod schemas for custom validation:
import { z } from "zod";
export const registerSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain an uppercase letter")
.regex(/[0-9]/, "Password must contain a number"),
});
export const customProfileSchema = z.object({
name: z.string().min(2),
company: z.string().optional(),
jobTitle: z.string().optional(),
website: z.string().url().optional(),
});Removing Optional Features
Remove Autumn (Billing)
If you don't need billing:
- Remove
AUTUMN_SECRET_KEYfrom environment variables - Delete billing-related routes in
/app/organizations/[organizationSlug]/billing/ - Remove billing UI components
- Update organization creation to not require billing
Remove Sentry (Error Monitoring)
Sentry is environment-variable driven — the SDK no-ops when
NEXT_PUBLIC_SENTRY_DSN is empty, and builds skip source-map upload when
SENTRY_AUTH_TOKEN is missing. To simply disable it, leave those variables unset;
no code changes are required.
To remove it entirely:
- Delete
instrumentation.ts,instrumentation-client.ts,sentry.edge.config.ts, andsentry.server.config.ts - Remove the Sentry wrapper from
next.config.js - Drop the
NEXT_PUBLIC_SENTRY_DSN/SENTRY_*env vars - Uninstall the package:
bun remove @sentry/nextjsRemove Passkeys
If you don't need passkey authentication:
- Remove passkey routes in
/app/account/security/passkeys/ - Remove passkey components
- Disable passkey plugin in Better Auth config:
export const auth = betterAuth({
// Remove or comment out passkey plugin
// plugins: [passkey()],
});Adding New Components
Create Reusable Components
Add to /components/ for shared components:
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface StatsCardProps {
title: string;
value: string | number;
description?: string;
icon?: React.ReactNode;
}
export function StatsCard({ title, value, description, icon }: StatsCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p className="text-xs text-muted-foreground">{description}</p>
)}
</CardContent>
</Card>
);
}Use Shadcn UI Components
Add new Shadcn components as needed:
bunx shadcn@latest add chart
bunx shadcn@latest add calendar
bunx shadcn@latest add commandCustom Hooks
Add custom hooks in /lib/hooks/:
"use client";
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
export function useOrganization(slug: string) {
const organization = useQuery(api.organizations.getBySlug, { slug });
const members = useQuery(api.organizations.getMembers, { slug });
return {
organization,
members,
isLoading: organization === undefined,
};
}Environment-Specific Configuration
Development vs Production
Use environment checks for different behavior:
const isDev = process.env.NODE_ENV === "development";
export const config = {
apiUrl: isDev
? "http://localhost:3000/api"
: "https://yourdomain.com/api",
enableDebugLogs: isDev,
mockPayments: isDev,
};Legal Pages (Terms & Privacy)
The template ships starter Terms of Service and Privacy Policy pages so you have something to link from the footer and the register screen on day one:
app/(legal)/
├── layout.tsx # Shared minimal legal layout
├── terms/page.tsx # Terms of Service (starter copy)
└── privacy/page.tsx # Privacy Policy (starter copy)Company/app name and the contact address are parameterized through
config/site.ts (appName, supportEmail), so you mostly edit copy rather than
markup.
The starter copy is a scaffold, not legal advice. Before going to production,
review and replace the Terms and Privacy copy with your own (ideally with counsel),
set NEXT_PUBLIC_SUPPORT_EMAIL to your real support address, and confirm the listed
data processors and retention/refund terms match your business.
Demo Video Tooling (Bonus)
The template includes a two-stage pipeline that turns the real app into an edited, launch-style demo video — a bonus you can use or delete.
# One-time setup
bun run demo:install-browsers # Playwright Chromium
cd video && bun install && cd .. # the Remotion project has its own deps
# With `bun convex dev` and `bun dev` running:
DEMO_EMAIL=you@example.com DEMO_PASSWORD=your-password bun run demo:renderbun run demo:capture— Playwright drives the app and captures real screens intovideo/public/demo(see the header comment inscripts/capture-demo.tsfor the full list ofDEMO_*env vars).bun run demo:video— renders the Remotion composition invideo/.bun run demo:render— runs both stages; output isdemo-output/unstack-pro-demo-latest.mp4.
The video/ Remotion project is a self-contained package with its own
dependencies — run bun install inside it once before rendering. See the product
README's "Scripted Demo Video" section for details.
Best Practices
- Don't modify core auth files unless necessary - extend instead
- Keep components small and focused on one responsibility
- Use TypeScript strictly for type safety
- Follow existing patterns for consistency
- Test changes locally before deploying
- Document custom code for future reference
- Use environment variables for all configuration
- Keep secrets secure - never commit them
Resources
- Next.js Documentation
- Convex Documentation
- better-convex Documentation - Our Better Auth + Convex integration
- Better Auth Documentation
- Tailwind CSS Documentation
- Shadcn UI Documentation