Cover image for Building a Financial Portfolio Tracker with Next.js, Supabase & Prisma

Building a Financial Portfolio Tracker with Next.js, Supabase & Prisma

Mon Mar 31 2025

SupabasePrismaNextJs

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.