1. Project Context
This article walks you through the architecture and implementation of a Financial Portfolio Tracker using Next.js, Supabase, and Prisma. This project simulates a potential interview challenge or take-home assignment—one that showcases how you think through solutions, structure your codebase, and leverage modern web tools.
You’ll learn how to:
- Use Supabase Auth for secure user authentication (via server actions).
- Use Prisma as the ORM for managing your database schema and queries.
- Build a responsive dashboard UI in Next.js to track portfolios and assets.
- Integrate React Query for client-side data fetching and cache management.
2. Problem Breakdown
The core requirements are:
- A user must be able to create multiple financial portfolios.
- Each portfolio contains a list of assets (e.g., stocks, cryptocurrencies).
- Users must be able to add/remove/edit assets in a portfolio.
- All portfolios and assets should be scoped to authenticated users.
Bonus ideas (future iterations):
- Asset history tracking
- Charts/graphs for portfolio value over time
- Currency conversion
- Real-time price fetching from external APIs
3. Tech Stack Justification
Choosing the right stack for a full-stack application like a financial portfolio tracker is crucial for balancing productivity, performance, and maintainability.
- Next.js (App Router): Server components, built-in routing, server actions, and API flexibility.
- Supabase: Provides instant infrastructure with Postgres, Auth, and Realtime features.
- Prisma: A modern ORM with TypeScript support and a smooth DX.
- React Query: Powerful data-fetching and caching solution for the frontend.
With Supabase for auth and Prisma for data modeling, we get the best of both worlds: Supabase manages sessions and roles, while Prisma provides clean query APIs and schema evolution tools.
4. System Architecture
A simplified overview of how components communicate:
- Frontend (Next.js + Server Actions): Uses React Server Components and server actions for secure mutations.
- Supabase Auth: Handles user login, signup, session persistence.
- Prisma (PostgreSQL via Supabase): Manages all DB interactions.
- React Query (on client): Manages client-side fetching, caching, and state syncing.
Authentication-aware operations use createServerClient()
from Supabase in server actions.
5. Database Schema Design (Prisma Models)
// prisma/schema.prisma
model User {
id String @id @default(uuid())
email String @unique
portfolios Portfolio[]
}
model Portfolio {
id String @id @default(uuid())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id])
assets Asset[]
}
model Asset {
id String @id @default(uuid())
symbol String
name String
amount Float
portfolioId String
portfolio Portfolio @relation(fields: [portfolioId], references: [id])
}
6. Supabase Authentication with Server Actions
// lib/supabase-server.ts
import { createServerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
export function createSupabaseServerClient() {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ cookies }
);
}
// app/actions/auth.ts
'use server';
import { createSupabaseServerClient } from '@/lib/supabase-server';
export async function loginUser(email: string, password: string) {
const supabase = createSupabaseServerClient();
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) throw new Error(error.message);
}
export async function signUpUser(email: string, password: string) {
const supabase = createSupabaseServerClient();
const { error } = await supabase.auth.signUp({ email, password });
if (error) throw new Error(error.message);
}
// lib/getUser.ts
import { createSupabaseServerClient } from './supabase-server';
export async function getUser() {
const supabase = createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return user;
}
7. Server Actions for CRUD (Portfolios & Assets)
// app/actions/portfolio.ts
'use server';
import prisma from '@/lib/prisma';
import { getUser } from '@/lib/getUser';
export async function createPortfolio(name: string) {
const user = await getUser();
if (!user) throw new Error('Unauthorized');
const portfolio = await prisma.portfolio.create({
data: {
name,
userId: user.id,
},
});
return portfolio;
}
export async function deletePortfolio(id: string) {
const user = await getUser();
if (!user) throw new Error('Unauthorized');
await prisma.portfolio.delete({
where: { id, userId: user.id },
});
}
// app/actions/asset.ts
'use server';
import prisma from '@/lib/prisma';
import { getUser } from '@/lib/getUser';
export async function addAsset(portfolioId: string, symbol: string, name: string, amount: number) {
const user = await getUser();
if (!user) throw new Error('Unauthorized');
const asset = await prisma.asset.create({
data: {
symbol,
name,
amount,
portfolio: {
connect: {
id: portfolioId,
},
},
},
});
return asset;
}
export async function removeAsset(id: string) {
const user = await getUser();
if (!user) throw new Error('Unauthorized');
await prisma.asset.delete({
where: { id },
});
}
8. React Query for Client-Side Fetching
Install dependencies:
npm install @tanstack/react-query
Setup provider:
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactNode, useState } from 'react';
export function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
Example usage to fetch portfolio details:
// lib/api.ts
export async function fetchPortfolios(): Promise<Portfolio[]> {
const res = await fetch('/api/portfolios');
if (!res.ok) throw new Error('Failed to fetch portfolios');
return res.json();
}
// components/PortfolioList.tsx
'use client';
import { useQuery } from '@tanstack/react-query';
import { fetchPortfolios } from '@/lib/api';
export function PortfolioList() {
const { data, error, isLoading } = useQuery({
queryKey: ['portfolios'],
queryFn: fetchPortfolios,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading portfolios</div>;
return (
<ul>
{data.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
9. Next Steps & Bonus Features
Here are some areas you can expand upon if you'd like to improve or showcase this project further:
- Integrate external APIs (e.g., Alpha Vantage, CoinGecko) for live price tracking.
- Build a chart component using Chart.js or Recharts.
- Implement asset history for value tracking.
- Add optimistic UI updates with React Query mutation hooks.
- Add role-based access (admin, read-only, etc.).
10. Final Thoughts
This project demonstrates how you can build a production-grade portfolio dashboard using full-stack TypeScript, Prisma, and server components with clean separation between the client and server layers. It gives you an edge in interviews by showing you understand modern practices like:
- Auth with secure server actions
- ORM-based schema and querying
- React Query for frontend data management
- Modular and scalable file structure in Next.js App Router
Use this as a base to build your own financial app, or adapt it to fit other domains like inventory, budgeting, or tracking.