Customization
Extend and customize Fastly for your application
Fastly is designed to be extended. This guide covers common customization scenarios.
Add OAuth Provider
Add a new OAuth provider (e.g., Twitter, Discord, LinkedIn):
Update AuthMethod enum
export enum AuthMethod {
EMAIL = 'email',
GOOGLE = 'google',
GITHUB = 'github',
TWITTER = 'twitter', // Add new provider
}Create callback route
import { NextRequest, NextResponse } from 'next/server';
import { connectDB } from '@/lib/config/db-connect';
import { UserAuth, User } from '@/models/users';
import { generateTokenPair } from '@/helpers/jwt-token';
import { createUserSession } from '@/lib/auth/session-tracker';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const code = searchParams.get('code');
const error = searchParams.get('error');
if (error || !code) {
return NextResponse.redirect(
new URL('/log-in?error=oauth_cancelled', process.env.NEXT_PUBLIC_APP_URL!)
);
}
try {
// 1. Exchange code for access token
const tokenResponse = await fetch('https://api.twitter.com/2/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
grant_type: 'authorization_code',
client_id: process.env.TWITTER_CLIENT_ID!,
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/oauth/twitter`,
code_verifier: 'challenge', // Implement PKCE
}),
});
const { access_token } = await tokenResponse.json();
// 2. Fetch user profile
const userResponse = await fetch('https://api.twitter.com/2/users/me', {
headers: { Authorization: `Bearer ${access_token}` },
});
const userData = await userResponse.json();
// 3. Create or login user
await connectDB();
let userAuth = await UserAuth.findOne({ email: userData.email });
if (!userAuth) {
// Create new user
userAuth = await UserAuth.create({
email: userData.email,
isVerified: true,
authMethod: 'twitter',
});
await User.create({
userAuth: userAuth._id,
email: userData.email,
firstName: userData.name.split(' ')[0],
lastName: userData.name.split(' ').slice(1).join(' '),
username: userData.username,
avatar: userData.profile_image_url,
});
}
// 4. Generate tokens and session
const tokens = generateTokenPair(userAuth._id.toString(), userAuth.email);
const session = await createUserSession({
userAuthId: userAuth._id,
authMethod: 'twitter',
request,
});
// 5. Redirect with tokens
const redirectUrl = new URL('/oauth-callback', process.env.NEXT_PUBLIC_APP_URL);
redirectUrl.searchParams.set('accessToken', tokens.accessToken);
redirectUrl.searchParams.set('refreshToken', tokens.refreshToken);
redirectUrl.searchParams.set('sessionId', session.sessionId);
return NextResponse.redirect(redirectUrl);
} catch (error) {
return NextResponse.redirect(
new URL('/log-in?error=oauth_failed', process.env.NEXT_PUBLIC_APP_URL!)
);
}
}Add OAuth button
'use client';
import { Button } from '@/components/ui/button';
import { FaTwitter } from 'react-icons/fa';
export function TwitterButton() {
const handleClick = () => {
const clientId = process.env.NEXT_PUBLIC_TWITTER_CLIENT_ID;
const redirectUri = `${window.location.origin}/api/oauth/twitter`;
const url = new URL('https://twitter.com/i/oauth2/authorize');
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', clientId!);
url.searchParams.set('redirect_uri', redirectUri);
url.searchParams.set('scope', 'users.read tweet.read');
url.searchParams.set('state', 'state');
url.searchParams.set('code_challenge', 'challenge');
url.searchParams.set('code_challenge_method', 'plain');
window.location.href = url.toString();
};
return (
<Button variant="outline" onClick={handleClick} className="w-full">
<FaTwitter className="mr-2 h-4 w-4" />
Continue with Twitter
</Button>
);
}Update environment variables
NEXT_PUBLIC_TWITTER_CLIENT_ID=your-client-id
TWITTER_CLIENT_SECRET=your-client-secretExtend User Model
Add custom fields to the user profile:
Update Mongoose schema
const userSchema = new mongoose.Schema({
// Existing fields...
// Add new fields
company: { type: String, default: null },
website: { type: String, default: null },
phoneNumber: { type: String, default: null },
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
});Update TypeScript types
export interface User {
// Existing fields...
company: string | null;
website: string | null;
phoneNumber: string | null;
role: 'user' | 'admin' | 'moderator';
}Update Zod validation
export const updateUserDetailsSchema = z.object({
// Existing fields...
company: z.string().max(100).nullable().optional(),
website: z.string().url().nullable().optional(),
phoneNumber: z.string().regex(/^\+?[0-9]{10,15}$/).nullable().optional(),
});Update API handler
The POST /api/user-details handler will automatically accept the new fields if they pass Zod validation.
Modify Password Requirements
Customize password complexity rules:
const passwordSchema = z
.string()
.min(12, 'Minimum 12 characters')
.max(128, 'Maximum 128 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[0-9]/, 'Must contain number')
.regex(/[!@#$%^&*(),.?":{}|<>]/, 'Must contain special character')
.refine(
(password) => !/(.)\1{2,}/.test(password),
'Cannot contain repeated characters'
);
export const createAccountSchema = z.object({
firstName: z.string().min(1).max(20),
lastName: z.string().min(1).max(20),
email: z.string().email(),
password: passwordSchema,
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});Add Protected Routes
Create new protected pages:
'use client';
import { useSession } from '@/hooks/auth/useSession';
export default function SettingsPage() {
const { user, isLoading } = useSession();
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>Settings</h1>
<p>Welcome, {user?.firstName}</p>
{/* Settings content */}
</div>
);
}The (protected) route group layout handles authentication:
'use client';
import { useRequireAuth } from '@/hooks/auth/useRequireAuth';
export default function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
useRequireAuth(); // Redirects to login if not authenticated
return <>{children}</>;
}Custom Email Templates
Email templates use React Email components:
Customize Verification Email
import {
Html,
Head,
Body,
Container,
Section,
Heading,
Text,
Button,
} from '@react-email/components';
interface VerificationEmailProps {
firstName: string;
code: string;
}
export function VerificationEmail({ firstName, code }: VerificationEmailProps) {
return (
<Html>
<Head />
<Body style={main}>
<Container style={container}>
<Section>
<Heading style={heading}>Verify your email</Heading>
<Text style={text}>Hi {firstName},</Text>
<Text style={text}>
Thanks for signing up! Use this code to verify your email:
</Text>
<Section style={codeContainer}>
<Text style={codeText}>{code}</Text>
</Section>
<Text style={text}>This code expires in 5 minutes.</Text>
<Text style={footer}>
If you didn't create an account, you can ignore this email.
</Text>
</Section>
</Container>
</Body>
</Html>
);
}
const main = {
backgroundColor: '#f6f9fc',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
};
const container = {
backgroundColor: '#ffffff',
margin: '0 auto',
padding: '40px',
borderRadius: '8px',
};
const heading = {
fontSize: '24px',
fontWeight: '600',
color: '#1a1a1a',
marginBottom: '24px',
};
const text = {
fontSize: '16px',
color: '#4a4a4a',
lineHeight: '24px',
};
const codeContainer = {
backgroundColor: '#f4f4f5',
borderRadius: '8px',
padding: '16px',
textAlign: 'center' as const,
margin: '24px 0',
};
const codeText = {
fontSize: '32px',
fontWeight: '700',
letterSpacing: '8px',
color: '#1a1a1a',
};
const footer = {
fontSize: '14px',
color: '#6b7280',
marginTop: '32px',
};Theming
CSS Variables
Customize the color palette in src/app/globals.css:
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--border: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
}User Theme Preference
The user's theme preference is stored in User.preferences.theme:
// Update theme preference
await userService.updateUserDetails({
preferences: {
theme: 'dark', // 'light' | 'dark' | 'system'
},
});Custom Hooks
Create custom hooks for your features:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { subscriptionService } from '@/services/subscription-service';
export function useSubscription() {
return useQuery({
queryKey: ['subscription'],
queryFn: () => subscriptionService.getSubscription(),
});
}
export function useCancelSubscription() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => subscriptionService.cancel(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['subscription'] });
},
});
}Add API Endpoints
Create new API routes following the existing pattern:
import { NextRequest } from 'next/server';
import { authenticate } from '@/lib/auth/auth-middleware';
import { sendSuccess, sendBadRequest } from '@/lib/utils/response';
import { connectDB } from '@/lib/config/db-connect';
export async function GET(request: NextRequest) {
const auth = await authenticate(request);
if (!auth.success) return auth.response;
await connectDB();
// Fetch subscription data
const subscription = await getSubscription(auth.user.userId);
return sendSuccess('Subscription fetched', { subscription });
}
export async function POST(request: NextRequest) {
const auth = await authenticate(request);
if (!auth.success) return auth.response;
const body = await request.json();
// Validate and process
// ...
return sendSuccess('Subscription updated');
}Use the authenticate middleware for protected endpoints. It validates the access token and session.