This document provides a comprehensive overview of the portfolio platform's architecture, design decisions, and technical implementation.
- System Overview
- Technology Stack
- Architecture Patterns
- Database Schema
- Authentication Flow
- Data Flow
- API Design
- Security
- Performance
- Deployment
The portfolio platform is a modern, full-stack web application built with Next.js 15, leveraging the App Router for optimal performance and developer experience.
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Public │ │ Admin │ │ API │ │
│ │ Pages │ │ Panel │ │ Routes │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼──────────────────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ Server │ │
│ │ Components │ │
│ └──────┬───────┘ │
└───────────────────────────┼──────────────────────────────────┘
│
┌───────▼────────┐
│ Prisma ORM │
└───────┬────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌───────▼────────┐ ┌──────▼───────┐ ┌───────▼────────┐
│ PostgreSQL │ │ Supabase │ │ Discord │
│ Database │ │ Auth │ │ Webhook │
└────────────────┘ └──────────────┘ └────────────────┘
- Monorepo Structure: Single repository with all code in
frontend/ - Server-First: Leverages React Server Components for optimal performance
- Type-Safe: Full TypeScript coverage with Prisma-generated types
- Zero API Layer: Direct database access from server components
- Modern Patterns: Server Actions for mutations, minimal client JavaScript
Next.js 15 (App Router)
- Why: Latest App Router provides superior performance with React Server Components
- Benefits:
- Direct database access from server components
- Automatic code splitting
- Built-in optimization (images, fonts, etc.)
- Streaming and Suspense support
- Server Actions for mutations
TypeScript 5.0
- Why: Type safety prevents runtime errors and improves DX
- Benefits:
- Catch errors at compile time
- Autocomplete and IntelliSense
- Self-documenting code
- Seamless integration with Prisma
Tailwind CSS
- Why: Utility-first approach enables rapid development
- Benefits:
- Consistent design system
- Dark mode support out of the box
- Minimal CSS bundle size
- Responsive utilities
- Easy customization
Prisma ORM
- Why: Best-in-class TypeScript ORM with excellent DX
- Benefits:
- Type-safe database queries
- Automatic migrations
- Database introspection
- Prisma Studio for GUI management
- Generated types match schema
PostgreSQL 15+
- Why: Robust, ACID-compliant relational database
- Benefits:
- Complex queries and relationships
- JSON support for flexible data
- Full-text search capabilities
- Excellent performance
- Supabase compatibility
Supabase Auth
- Why: Production-ready auth with minimal setup
- Benefits:
- Email/password authentication
- Email verification
- Session management
- JWT tokens
- Row-level security (RLS)
Supabase Storage
- Why: Integrated storage solution with PostgreSQL
- Benefits:
- S3-compatible API
- CDN integration
- Access control policies
- Automatic optimization
- Same provider as database
Vercel
- Why: Built by Next.js creators, optimized for the framework
- Benefits:
- Zero-config deployment
- Edge network
- Automatic HTTPS
- Preview deployments
- Environment variables
- Analytics and monitoring
All pages and components are Server Components by default, enabling direct database access.
// app/projects/page.tsx
import { prisma } from '@/lib/prisma'
export default async function ProjectsPage() {
// Direct database access - no API call needed
const projects = await prisma.project.findMany({
where: { published: true },
orderBy: { displayOrder: 'asc' }
})
return <ProjectGrid projects={projects} />
}Benefits:
- No API roundtrip latency
- Reduced client JavaScript
- Better SEO (fully rendered HTML)
- Automatic data caching
Forms and mutations use Server Actions instead of API routes.
// app/actions/projects.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createProject(formData: FormData) {
const project = await prisma.project.create({
data: {
title: formData.get('title'),
// ... other fields
}
})
revalidatePath('/projects')
return { success: true, data: project }
}Benefits:
- No API route needed
- Type-safe parameters
- Automatic revalidation
- Progressive enhancement
Only interactive components are marked with 'use client'.
// components/ThemeToggle.tsx
'use client'
import { useTheme } from 'next-themes'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<button onClick={() => setTheme('dark')}>
Toggle Theme
</button>
)
}When to use:
- Event handlers (onClick, onChange, etc.)
- React hooks (useState, useEffect, etc.)
- Browser APIs (localStorage, window, etc.)
- Third-party libraries requiring client-side
API routes only for external access or webhooks.
// app/api/contact/route.ts
export async function POST(req: Request) {
const body = await req.json()
// Save to database
await prisma.contact.create({ data: body })
// Send Discord notification
await sendDiscordNotification(body)
return Response.json({ success: true })
}Use cases:
- Contact form submissions
- Webhooks (Discord, Stripe, etc.)
- Public APIs
- Third-party integrations
┌─────────────┐
│ Profile │
│ (Singleton)│
└─────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Project │ │ BlogPost │ │ Publication │
│ │ │ │ │ │
│ - slug │ │ - slug │ │ - slug │
│ - published │ │ - published │ │ - published │
│ - order │ │ - tags[] │ │ - authors[] │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│WorkExperience│ │ Education │ │ Skill │
│ │ │ │ │ │
│ - current │ │ - current │ │ - category │
│ - order │ │ - order │ │ - proficiency│
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐
│ Contact │ │ User │
│ │ │ (Supabase) │
│ - read │ │ - email │
│ - createdAt │ │ - role │
└─────────────┘ └─────────────┘
- UUID Primary Keys: Better for distributed systems and security
- JSON Arrays: For flexible lists (tags, authors, achievements)
- Slug Fields: SEO-friendly URLs for public content
- Published Flags: Draft/publish workflow
- Display Order: Manual sorting control
- Timestamps: Track creation and updates
- Indexes: Optimized for common queries
model Profile {
id String @id @default(dbgenerated("gen_random_uuid()"))
name String
email String
githubUrl String?
linkedinUrl String?
resumeUrl String?
// ... other fields
}
model Project {
id String @id @default(dbgenerated("gen_random_uuid()"))
slug String @unique
title String
published Boolean @default(false)
displayOrder Int @default(0)
techStack String[] // Array of technologies
// ... other fields
@@index([published, displayOrder])
}
model BlogPost {
id String @id @default(dbgenerated("gen_random_uuid()"))
slug String @unique
title String
published Boolean @default(false)
publishedAt DateTime?
tags String[] // Array of tags
// ... other fields
@@index([published, publishedAt])
}┌──────┐ ┌───────────┐ ┌─────────────┐ ┌──────────┐
│User │ │ Signup │ │ Supabase │ │Database │
│ │ │ Page │ │ Auth │ │ │
└──┬───┘ └─────┬─────┘ └──────┬──────┘ └────┬─────┘
│ │ │ │
│ Navigate │ │ │
├───────────────>│ │ │
│ │ │ │
│ │ Check if users │ │
│ │ exist │ │
│ ├───────────────────>│ │
│ │ │ Query users │
│ │ ├─────────────────>│
│ │ │<─────────────────┤
│ │<───────────────────┤ │
│ │ │ │
│ Show form or │ │ │
│ "Access Denied" │ │
│<───────────────┤ │ │
│ │ │ │
│ Submit form │ │ │
├───────────────>│ │ │
│ │ Create account │ │
│ ├───────────────────>│ │
│ │ │ Store user │
│ │ ├─────────────────>│
│ │ Send verification │ │
│ │ email │ │
│<───────────────┼────────────────────┤ │
┌──────┐ ┌───────────┐ ┌─────────────┐ ┌──────────┐
│User │ │ Login │ │ Supabase │ │Middleware│
│ │ │ Page │ │ Auth │ │ (Proxy) │
└──┬───┘ └─────┬─────┘ └──────┬──────┘ └────┬─────┘
│ │ │ │
│ Submit creds │ │ │
├───────────────>│ │ │
│ │ Sign in │ │
│ ├───────────────────>│ │
│ │ │ │
│ │ JWT + session │ │
│ │<───────────────────┤ │
│ │ Set cookies │ │
│<───────────────┤ │ │
│ │ │ │
│ Navigate to │ │ │
│ /admin │ │ │
├───────────────────────────────────────────────────────>│
│ │ │ Verify session │
│ │ │<─────────────────┤
│ │ │ Valid session │
│ │ ├─────────────────>│
│ Render admin │ │ │
│<────────────────────────────────────────────────────────┤
// src/proxy.ts
export async function proxy(request: NextRequest) {
const supabase = createServerClient(/* ... */)
// Refresh session
const { data: { session } } = await supabase.auth.getSession()
// Protect /admin routes
if (request.nextUrl.pathname.startsWith('/admin')) {
const isAuthPage = pathname === '/admin/login' || pathname === '/admin/signup'
if (!session && !isAuthPage) {
// Redirect to login
return NextResponse.redirect(new URL('/admin/login', request.url))
}
if (session && isAuthPage) {
// Already logged in, redirect to dashboard
return NextResponse.redirect(new URL('/admin', request.url))
}
}
return response
}User Request
↓
Next.js Server
↓
Server Component
↓
Direct Prisma Query ──→ PostgreSQL
↓
Render HTML
↓
Send to Client
User Submits Form
↓
Client Component (with Server Action)
↓
Server Action
↓
Prisma Mutation ──→ PostgreSQL
↓
Revalidate Cache
↓
Return Result
↓
Update UI
User Submits Form
↓
POST /api/contact
↓
┌─────────────┬──────────────┐
│ │ │
Save to DB Send Discord │
↓ Notification │
PostgreSQL Discord API │
│ │ │
└─────────────┴──────────┘
↓
Return Success
↓
Show Confirmation
GET /api/profile # Get profile data
PUT /api/profile # Update profile
POST /api/contact # Submit contact form
// app/actions/projects.ts
export async function createProject(data)
export async function updateProject(id, data)
export async function deleteProject(id)
export async function publishProject(id)
// app/actions/blog.ts
export async function createPost(data)
export async function updatePost(id, data)
export async function deletePost(id)
// app/actions/contacts.ts
export async function getContacts()
export async function markAsRead(id)
export async function deleteContact(id)Success Response:
{
success: true,
data: { /* entity */ },
message?: "Operation successful"
}Error Response:
{
success: false,
error: "Error message",
details?: { /* validation errors */ }
}- Single Admin Account: Enforced at signup - only one account allowed
- Email Verification: Required before login
- JWT Tokens: Secure session management via Supabase
- HTTP-Only Cookies: Tokens stored securely, not accessible to JavaScript
- Middleware Protection: All
/adminroutes protected by proxy middleware
// Server-side validation
export async function createProject(data: FormData) {
// Validate required fields
if (!data.get('title') || !data.get('slug')) {
return { success: false, error: 'Missing required fields' }
}
// Sanitize inputs
const title = sanitize(data.get('title'))
// Prevent SQL injection (Prisma handles this)
await prisma.project.create({ data: { title } })
}Public (NEXT_PUBLIC_*):
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_SITE_URL
Private (Server-only):
DATABASE_URLSUPABASE_SERVICE_ROLE_KEYDISCORD_WEBHOOK_URL
// API routes include CORS headers
headers: {
'Access-Control-Allow-Origin': process.env.NEXT_PUBLIC_SITE_URL,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}Implemented at edge level via Vercel:
- Protects against brute force attacks
- Limits API requests per IP
- Configurable per route
-
React Server Components
- Zero client JavaScript for static content
- Direct database access
- Streaming and Suspense
-
Incremental Static Regeneration (ISR)
export const revalidate = 60 // Revalidate every 60 seconds
-
Image Optimization
- Next.js automatic image optimization
- WebP format conversion
- Responsive images
- Lazy loading
-
Code Splitting
- Automatic route-based splitting
- Dynamic imports for heavy components
- Minimal client JavaScript
-
Database Indexes
@@index([published, displayOrder]) @@index([published, publishedAt])
-
Edge Caching
- Vercel Edge Network
- Cached at 300+ global locations
- Automatic cache invalidation
Target Metrics:
- Lighthouse Score: 95+
- First Contentful Paint: < 1.5s
- Time to Interactive: < 3.5s
- Cumulative Layout Shift: < 0.1
┌────────────────────────────────────────────────────┐
│ Vercel Edge Network │
│ ┌──────────────────────────────────────────────┐ │
│ │ CDN (300+ locations) │ │
│ │ - Static assets │ │
│ │ - Cached pages │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────┬─────────────────────────────────┘
│
┌──────────▼──────────┐
│ Next.js Server │
│ (Serverless) │
└──────────┬──────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌───▼────┐ ┌─────▼─────┐ ┌────▼────┐
│Supabase│ │ Supabase │ │ Discord │
│ DB │ │ Auth │ │Webhooks │
└────────┘ └───────────┘ └─────────┘
-
Code Push
git push origin main
-
Automatic Build (Vercel)
- Install dependencies
- Run type checking
- Build production bundle
- Generate static pages
-
Deploy to Edge
- Deploy to global edge network
- Update DNS
- Automatic HTTPS
-
Database Migration (if needed)
npx prisma migrate deploy
Development:
- Local PostgreSQL (Docker)
- Local Supabase emulator (optional)
- Test Discord webhook
Production:
- Supabase PostgreSQL
- Supabase Auth
- Production Discord webhook
Vercel Analytics:
- Real User Monitoring (RUM)
- Web Vitals tracking
- Error tracking
- Performance insights
Logs:
- Server logs via Vercel dashboard
- Database logs via Supabase
- Custom logging with
console.log(development)
- Database: Supabase automated daily backups
- Code: Git version control
- Environment:
.env.local.examplein repository - Content: Periodic Prisma Studio exports
- Serverless Functions: Auto-scale with traffic
- Edge Network: Distributed globally
- Database: Supabase handles connection pooling
// Route-level caching
export const revalidate = 60
// Per-request caching
const projects = await prisma.project.findMany({
where: { published: true },
// Cached for 1 hour
cache: 'force-cache',
next: { revalidate: 3600 }
})- Full-text Search: PostgreSQL full-text search or Algolia
- CDN for Media: Cloudinary or Imgix integration
- Email Service: SendGrid or Resend for transactional emails
- Analytics: PostHog or Plausible for privacy-friendly analytics
- CMS Integration: Sanity or Contentful for non-technical users
This architecture provides a modern, scalable, and maintainable foundation for a personal portfolio platform. Key benefits:
- Developer Experience: Type-safe, minimal boilerplate, fast iteration
- Performance: Server-first approach, edge caching, optimized assets
- Cost-Effective: Free tier covers most use cases ($0-27/month)
- Secure: Industry-standard auth, input validation, secure secrets
- Scalable: Serverless architecture, global CDN, efficient caching
The monolithic Next.js approach eliminates complexity of separate frontend/backend while maintaining flexibility through server components, server actions, and API routes where needed.