Skip to main content

Development Workflow

Best practices and workflow for OneLibro development.

Table of Contents


Getting Started

Initial Setup

  1. Clone repository:
git clone https://github.com/Yatheesh-Nagella/yatheesh-portfolio.git
cd yatheesh-portfolio
  1. Install dependencies:
npm install
  1. Set up environment variables:
cp .env.example .env
# Edit .env with your actual credentials
  1. Run development server:
npm run dev
  1. Open in browser:

Project Structure

yatheesh-portfolio/
├── app/ # Next.js App Router pages
│ ├── finance/ # Finance app routes
│ │ ├── dashboard/ # Dashboard page
│ │ ├── accounts/ # Accounts management
│ │ ├── transactions/ # Transaction history
│ │ ├── budgets/ # Budget management
│ │ ├── settings/ # User settings
│ │ ├── login/ # Authentication
│ │ ├── layout.tsx # Finance app layout
│ │ └── page.tsx # Finance landing page
│ ├── admin/ # Admin dashboard routes
│ │ ├── users/ # User management
│ │ ├── invites/ # Invite code management
│ │ ├── emails/ # Email system
│ │ ├── layout.tsx # Admin layout
│ │ └── page.tsx # Admin dashboard
│ ├── api/ # API endpoints
│ │ ├── plaid/ # Plaid integration endpoints
│ │ ├── admin/ # Admin-only endpoints
│ │ └── cron/ # Cron job endpoints
│ ├── blogs/ # Portfolio blog posts
│ ├── page.jsx # Main portfolio page
│ └── layout.tsx # Root layout
├── components/ # React components
│ ├── finance/ # Finance-specific components
│ │ ├── DashboardCard.tsx
│ │ ├── PlaidLink.tsx
│ │ ├── SpendingChart.tsx
│ │ ├── AccountCard.tsx
│ │ ├── BudgetForm.tsx
│ │ └── TransactionForm.tsx
│ ├── KofiButton.jsx # Ko-fi donation widget
│ └── NewsletterSignup.jsx
├── contexts/ # React Context providers
│ └── AuthContext.tsx # Authentication state
├── lib/ # Utility libraries
│ ├── supabase.ts # Supabase client and helpers
│ ├── plaid.ts # Plaid API wrapper
│ ├── email.ts # Email sending utilities
│ ├── env.ts # Environment variable validation
│ └── utils.ts # Generic utilities
├── types/ # TypeScript type definitions
│ ├── database.types.ts # Database table types
│ └── supabase.ts # Supabase generated types
├── public/ # Static assets
│ ├── images/ # Images and icons
│ └── favicon.ico # Favicon
├── DOCS/ # Internal documentation
│ ├── phase8-completion-summary.md
│ └── ...
├── supabase/ # Database migrations
│ └── migrations/ # SQL migration files
├── middleware.ts # Next.js middleware (subdomain routing)
├── next.config.ts # Next.js configuration
├── tailwind.config.ts # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
├── package.json # Dependencies and scripts
└── .env.local # Environment variables (gitignored)

Path Aliases

OneLibro uses TypeScript path aliases configured in tsconfig.json:

// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}

Usage:

// ✅ Good: Using path alias
import { supabase } from '@/lib/supabase';
import { useAuth } from '@/contexts/AuthContext';
import { DashboardCard } from '@/components/finance/DashboardCard';
import type { User } from '@/types/database.types';

// ❌ Bad: Relative paths
import { supabase } from '../../../lib/supabase';
import { useAuth } from '../../contexts/AuthContext';

Benefits:

  • Shorter, more readable imports
  • Easy to move files without updating imports
  • Clearer separation of local vs external imports

Common Development Tasks

Adding a New Feature

Step-by-Step Example: Adding a "Transaction Categories" feature

1. Plan the Feature:

  • Define requirements: Users can create custom categories
  • Design database schema: custom_categories table
  • Plan UI: Category management page
  • List API endpoints needed

2. Create Database Table:

-- supabase/migrations/20250124_create_custom_categories.sql
CREATE TABLE custom_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT,
color TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE custom_categories ENABLE ROW LEVEL SECURITY;

-- Users can only access their own categories
CREATE POLICY "Users can view own categories"
ON custom_categories FOR SELECT
USING (auth.uid() = user_id);

CREATE POLICY "Users can insert own categories"
ON custom_categories FOR INSERT
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own categories"
ON custom_categories FOR UPDATE
USING (auth.uid() = user_id);

CREATE POLICY "Users can delete own categories"
ON custom_categories FOR DELETE
USING (auth.uid() = user_id);

-- Admins can do everything
CREATE POLICY "Admins have full access"
ON custom_categories FOR ALL
USING (
EXISTS (
SELECT 1 FROM users
WHERE users.id = auth.uid() AND users.is_admin = true
)
);

-- Create index for performance
CREATE INDEX idx_custom_categories_user_id ON custom_categories(user_id);

3. Update TypeScript Types:

// types/database.types.ts
export interface CustomCategory {
id: string;
user_id: string;
name: string;
icon: string | null;
color: string | null;
created_at: string;
updated_at: string;
}

4. Create Helper Functions:

// lib/supabase.ts
export async function getUserCategories(userId: string): Promise<CustomCategory[]> {
const { data, error } = await supabase
.from('custom_categories')
.select('*')
.eq('user_id', userId)
.order('name');

if (error) {
console.error('Error fetching categories:', error);
return [];
}

return data || [];
}

export async function createCategory(category: {
userId: string;
name: string;
icon?: string;
color?: string;
}): Promise<{ data: CustomCategory | null; error: Error | null }> {
const { data, error } = await supabase
.from('custom_categories')
.insert({
user_id: category.userId,
name: category.name,
icon: category.icon,
color: category.color,
})
.select()
.single();

if (error) {
return { data: null, error: new Error(error.message) };
}

return { data, error: null };
}

5. Create API Endpoint (if needed):

// app/api/categories/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase';
import { getUserCategories, createCategory } from '@/lib/supabase';

export async function GET(request: NextRequest) {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const categories = await getUserCategories(user.id);
return NextResponse.json({ categories });
}

export async function POST(request: NextRequest) {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const body = await request.json();
const { name, icon, color } = body;

if (!name) {
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
}

const { data, error } = await createCategory({
userId: user.id,
name,
icon,
color,
});

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}

return NextResponse.json({ category: data }, { status: 201 });
}

6. Create UI Component:

// components/finance/CategoryForm.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

interface CategoryFormProps {
onSuccess?: () => void;
}

export function CategoryForm({ onSuccess }: CategoryFormProps) {
const router = useRouter();
const [name, setName] = useState('');
const [icon, setIcon] = useState('');
const [color, setColor] = useState('#22c55e');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);

try {
const response = await fetch('/api/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, icon, color }),
});

if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to create category');
}

setName('');
setIcon('');
setColor('#22c55e');
router.refresh();
onSuccess?.();
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong');
} finally {
setLoading(false);
}
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{error}
</div>
)}

<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Category Name
</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>

<div>
<label htmlFor="icon" className="block text-sm font-medium text-gray-700">
Icon (emoji or text)
</label>
<input
id="icon"
type="text"
value={icon}
onChange={(e) => setIcon(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
</div>

<div>
<label htmlFor="color" className="block text-sm font-medium text-gray-700">
Color
</label>
<input
id="color"
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="mt-1 block w-full h-10 rounded-md border-gray-300"
/>
</div>

<button
type="submit"
disabled={loading}
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:opacity-50"
>
{loading ? 'Creating...' : 'Create Category'}
</button>
</form>
);
}

7. Create Page:

// app/finance/categories/page.tsx
import { CategoryForm } from '@/components/finance/CategoryForm';
import { getUserCategories } from '@/lib/supabase';
import { getCurrentUser } from '@/lib/supabase';
import { redirect } from 'next/navigation';

export default async function CategoriesPage() {
const { user } = await getCurrentUser();

if (!user) {
redirect('/finance/login');
}

const categories = await getUserCategories(user.id);

return (
<div className="max-w-4xl mx-auto py-8 px-4">
<h1 className="text-3xl font-bold mb-8">Custom Categories</h1>

<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h2 className="text-xl font-semibold mb-4">Your Categories</h2>
<div className="space-y-2">
{categories.length === 0 ? (
<p className="text-gray-500">No custom categories yet</p>
) : (
categories.map((category) => (
<div
key={category.id}
className="flex items-center gap-3 p-3 bg-white rounded-lg border"
>
{category.icon && <span className="text-2xl">{category.icon}</span>}
<span
className="w-4 h-4 rounded-full"
style={{ backgroundColor: category.color || '#gray' }}
/>
<span className="font-medium">{category.name}</span>
</div>
))
)}
</div>
</div>

<div>
<h2 className="text-xl font-semibold mb-4">Add New Category</h2>
<CategoryForm />
</div>
</div>
</div>
);
}

8. Test the Feature:

  • Restart dev server
  • Navigate to /finance/categories
  • Create a category
  • Verify it appears in list
  • Check database to confirm data saved
  • Test RLS (try accessing another user's categories)

Adding a New API Endpoint

Step-by-Step Example: Adding an endpoint to get spending by category

1. Create API Route File:

// app/api/analytics/spending-by-category/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase';

export async function GET(request: NextRequest) {
// 1. Authenticate user
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

// 2. Parse query parameters
const { searchParams } = new URL(request.url);
const startDate = searchParams.get('startDate');
const endDate = searchParams.get('endDate');

// 3. Validate input
if (!startDate || !endDate) {
return NextResponse.json(
{ error: 'startDate and endDate are required' },
{ status: 400 }
);
}

try {
// 4. Query database
const { data: transactions, error } = await supabase
.from('transactions')
.select('category, amount')
.eq('user_id', user.id)
.gte('transaction_date', startDate)
.lte('transaction_date', endDate)
.gt('amount', 0); // Only expenses

if (error) throw error;

// 5. Process data
const spendingByCategory = transactions.reduce((acc, tx) => {
const category = tx.category || 'Uncategorized';
acc[category] = (acc[category] || 0) + tx.amount;
return acc;
}, {} as Record<string, number>);

// 6. Format response
const result = Object.entries(spendingByCategory)
.map(([category, amount]) => ({
category,
amount,
percentage: 0, // Will calculate below
}))
.sort((a, b) => b.amount - a.amount);

const total = result.reduce((sum, item) => sum + item.amount, 0);
result.forEach((item) => {
item.percentage = total > 0 ? (item.amount / total) * 100 : 0;
});

// 7. Return response
return NextResponse.json({
data: result,
total,
startDate,
endDate,
});

} catch (error) {
console.error('Error in spending-by-category:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}

2. Test Endpoint:

# Using curl
curl "http://localhost:3000/api/analytics/spending-by-category?startDate=2025-01-01&endDate=2025-01-31" \
-H "Cookie: sb-access-token=YOUR_TOKEN"

# Expected response:
# {
# "data": [
# { "category": "Food & Drink", "amount": 45000, "percentage": 45 },
# { "category": "Shopping", "amount": 30000, "percentage": 30 },
# ...
# ],
# "total": 100000,
# "startDate": "2025-01-01",
# "endDate": "2025-01-31"
# }

3. Create Client Hook (optional):

// hooks/useSpendingByCategory.ts
import useSWR from 'swr';

interface SpendingData {
category: string;
amount: number;
percentage: number;
}

export function useSpendingByCategory(startDate: string, endDate: string) {
const { data, error, isLoading } = useSWR<{
data: SpendingData[];
total: number;
}>(
`/api/analytics/spending-by-category?startDate=${startDate}&endDate=${endDate}`
);

return {
spendingData: data?.data || [],
total: data?.total || 0,
isLoading,
error,
};
}

4. Use in Component:

'use client';

import { useSpendingByCategory } from '@/hooks/useSpendingByCategory';

export function SpendingBreakdown() {
const { spendingData, total, isLoading } = useSpendingByCategory(
'2025-01-01',
'2025-01-31'
);

if (isLoading) return <div>Loading...</div>;

return (
<div>
<h2>Spending by Category</h2>
{spendingData.map((item) => (
<div key={item.category}>
{item.category}: ${(item.amount / 100).toFixed(2)} ({item.percentage.toFixed(1)}%)
</div>
))}
</div>
);
}

Adding a New Email Template

Step-by-Step Example: Adding a "Budget Exceeded" email

1. Create Email Template Component:

// emails/BudgetExceededEmail.tsx
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Section,
Text,
} from '@react-email/components';

interface BudgetExceededEmailProps {
userName: string;
budgetName: string;
budgetAmount: number;
currentSpending: number;
percentageOver: number;
}

export function BudgetExceededEmail({
userName,
budgetName,
budgetAmount,
currentSpending,
percentageOver,
}: BudgetExceededEmailProps) {
return (
<Html>
<Head />
<Preview>Your {budgetName} budget has been exceeded</Preview>
<Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'Arial, sans-serif' }}>
<Container style={{ margin: '0 auto', padding: '20px 0', maxWidth: '600px' }}>
<Heading style={{ color: '#ef4444', fontSize: '24px' }}>
Budget Exceeded
</Heading>

<Text style={{ fontSize: '16px', color: '#374151' }}>
Hi {userName},
</Text>

<Text style={{ fontSize: '16px', color: '#374151' }}>
Your <strong>{budgetName}</strong> budget has been exceeded.
</Text>

<Section style={{ padding: '20px', backgroundColor: '#fff', borderRadius: '8px' }}>
<Text style={{ fontSize: '14px', color: '#6b7280', margin: '5px 0' }}>
Budget Amount: ${(budgetAmount / 100).toFixed(2)}
</Text>
<Text style={{ fontSize: '14px', color: '#6b7280', margin: '5px 0' }}>
Current Spending: ${(currentSpending / 100).toFixed(2)}
</Text>
<Text style={{ fontSize: '18px', color: '#ef4444', fontWeight: 'bold', margin: '10px 0' }}>
You're {percentageOver.toFixed(1)}% over budget
</Text>
</Section>

<Button
href="https://finance.yatheeshnagella.com/budgets"
style={{
backgroundColor: '#22c55e',
color: '#ffffff',
padding: '12px 24px',
borderRadius: '6px',
textDecoration: 'none',
display: 'inline-block',
marginTop: '20px',
}}
>
View Budget Details
</Button>

<Text style={{ fontSize: '14px', color: '#6b7280', marginTop: '30px' }}>
Best regards,
<br />
The OneLibro Team
</Text>
</Container>
</Body>
</Html>
);
}

2. Add Email Sending Function:

// lib/email.ts
import { Resend } from 'resend';
import { BudgetExceededEmail } from '@/emails/BudgetExceededEmail';
import { env } from '@/lib/env';

const resend = new Resend(env.email.apiKey);

export async function sendBudgetExceededEmail({
to,
userName,
budgetName,
budgetAmount,
currentSpending,
}: {
to: string;
userName: string;
budgetName: string;
budgetAmount: number;
currentSpending: number;
}) {
const percentageOver = ((currentSpending - budgetAmount) / budgetAmount) * 100;

try {
const { data, error } = await resend.emails.send({
from: `${env.email.fromName} <${env.email.fromEmail}>`,
to,
subject: `Budget Alert: ${budgetName} Exceeded`,
react: BudgetExceededEmail({
userName,
budgetName,
budgetAmount,
currentSpending,
percentageOver,
}),
});

if (error) {
console.error('Error sending budget exceeded email:', error);
return { success: false, error };
}

console.log('Budget exceeded email sent:', data?.id);
return { success: true, data };
} catch (error) {
console.error('Failed to send budget exceeded email:', error);
return { success: false, error };
}
}

3. Use in Application:

// app/api/cron/budget-alerts/route.ts
import { sendBudgetExceededEmail } from '@/lib/email';

// Inside budget check loop
if (percentageSpent > 100) {
await sendBudgetExceededEmail({
to: user.email,
userName: user.full_name || 'there',
budgetName: budget.name,
budgetAmount: budget.amount_cents,
currentSpending: totalSpent,
});
}

4. Test Email Template:

# Preview email locally
npm run email:dev

# Opens React Email preview at http://localhost:3000
# Navigate to BudgetExceededEmail to see preview

Creating New Components

Step-by-Step: Creating a reusable "StatCard" component

1. Create Component File:

// components/finance/StatCard.tsx
import React from 'react';
import { TrendingUp, TrendingDown } from 'lucide-react';

interface StatCardProps {
title: string;
value: string;
subtitle?: string;
icon?: React.ReactNode;
trend?: {
value: number;
direction: 'up' | 'down';
};
className?: string;
}

export function StatCard({
title,
value,
subtitle,
icon,
trend,
className = '',
}: StatCardProps) {
return (
<div className={`bg-white rounded-lg border border-gray-200 p-6 ${className}`}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-600 uppercase tracking-wide">
{title}
</span>
{icon && <div className="text-green-600">{icon}</div>}
</div>

<div className="flex items-baseline justify-between">
<h3 className="text-3xl font-bold text-gray-900">{value}</h3>

{trend && (
<div className={`flex items-center gap-1 text-sm font-medium ${
trend.direction === 'up' ? 'text-green-600' : 'text-red-600'
}`}>
{trend.direction === 'up' ? (
<TrendingUp className="w-4 h-4" />
) : (
<TrendingDown className="w-4 h-4" />
)}
<span>{Math.abs(trend.value)}%</span>
</div>
)}
</div>

{subtitle && (
<p className="mt-2 text-sm text-gray-500">{subtitle}</p>
)}
</div>
);
}

2. Export from Barrel File:

// components/finance/index.ts
export { StatCard } from './StatCard';
export { DashboardCard } from './DashboardCard';
export { PlaidLink } from './PlaidLink';
// ... other exports

3. Use Component:

import { StatCard } from '@/components/finance';
import { DollarSign } from 'lucide-react';

export function Dashboard() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard
title="Total Balance"
value="$12,543.67"
subtitle="Across all accounts"
icon={<DollarSign className="w-5 h-5" />}
trend={{ value: 5.2, direction: 'up' }}
/>

<StatCard
title="Monthly Spending"
value="$2,456.18"
subtitle="Last 30 days"
trend={{ value: 12.3, direction: 'down' }}
/>

<StatCard
title="Connected Accounts"
value="5"
subtitle="2 banks linked"
/>
</div>
);
}

Git Workflow

Branch Naming

Use descriptive branch names with prefixes:

# Feature branches
feature/user-authentication
feature/plaid-integration
feature/budget-tracking

# Bug fixes
fix/login-redirect-issue
fix/transaction-sync-error

# Refactoring
refactor/optimize-queries
refactor/split-large-components

# Documentation
docs/api-reference
docs/setup-guide

# Chores (dependencies, config, etc.)
chore/update-dependencies
chore/configure-eslint

Creating a Feature Branch

# Update main branch
git checkout main
git pull origin main

# Create and switch to new branch
git checkout -b feature/your-feature-name

# Make changes and commit
git add .
git commit -m "feat: add user dashboard"

# Push to remote
git push -u origin feature/your-feature-name

Commit Messages

Follow Conventional Commits format:

<type>(<scope>): <subject>

<body>

<footer>

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes (formatting, no logic change)
  • refactor: Code refactoring (no feature or bug change)
  • perf: Performance improvements
  • test: Adding or updating tests
  • chore: Maintenance tasks (dependencies, config)

Examples:

# Feature
git commit -m "feat(auth): add invite-only signup system"

# Bug fix
git commit -m "fix(plaid): handle expired access tokens"

# Refactor
git commit -m "refactor(queries): optimize getUserAccounts query

- Use single query with nested relations instead of two queries
- Reduces database round trips from 2 to 1
- 50% performance improvement"

# Documentation
git commit -m "docs: add API reference for Plaid endpoints"

# Performance
git commit -m "perf(transactions): add pagination for transaction list"

Commit Message Best Practices:

Good:

feat(budgets): add monthly budget tracking

- Create budgets table with RLS policies
- Add createBudget API endpoint
- Implement budget creation UI
- Calculate spending vs budget automatically

Bad:

Update files

Bad:

WIP

Bad:

Fix bug

Pull Request Process

  1. Create pull request:
# Push your branch
git push origin feature/your-feature-name

# Create PR on GitHub
# Use PR template if available
  1. PR Title Format:
feat(scope): Brief description of changes
  1. PR Description Template:
## Summary
Brief description of what this PR does.

## Changes
- List key changes
- One item per line
- Be specific

## Test Plan
- [ ] Tested locally
- [ ] Added unit tests
- [ ] Tested in staging
- [ ] Manual testing steps:
1. Go to X page
2. Click Y button
3. Verify Z happens

## Screenshots (if applicable)
[Add screenshots or screen recordings]

## Related Issues
Fixes #123
Related to #456
  1. Before merging:
  • ✅ All CI checks pass
  • ✅ Code reviewed and approved
  • ✅ No merge conflicts
  • ✅ Branch is up to date with main
  1. Merge strategy:
# Squash and merge (preferred for feature branches)
# Creates single clean commit in main

# Rebase and merge (for clean linear history)
# Maintains individual commits

# Merge commit (for important milestones)
# Preserves full branch history

Code Standards

TypeScript

Always use TypeScript for new files:

// ✅ Good: Type-safe function
export async function getUserAccounts(userId: string): Promise<Account[]> {
// Implementation
}

// ❌ Bad: Using 'any'
export async function getUserAccounts(userId: any): Promise<any> {
// Implementation
}

Prefer interfaces for object types:

// ✅ Good: Interface
interface User {
id: string;
email: string;
full_name: string | null;
}

// ✅ Also good: Type alias (for unions, primitives)
type Status = 'active' | 'inactive' | 'pending';

// ❌ Avoid: Inline types
function updateUser(user: { id: string; email: string }) {
// Use interface instead
}

Use type imports:

// ✅ Good: Type-only import
import type { User, Account } from '@/types/database.types';

// ✅ Good: Mixed import
import { supabase } from '@/lib/supabase';
import type { Database } from '@/types/supabase';

React Components

Use functional components with hooks:

// ✅ Good: Functional component with TypeScript
interface DashboardCardProps {
title: string;
value: number;
icon: React.ReactNode;
trend?: number;
}

export function DashboardCard({ title, value, icon, trend }: DashboardCardProps) {
return (
<div className="bg-white rounded-lg p-6">
<div className="flex items-center justify-between">
<span className="text-gray-600">{title}</span>
{icon}
</div>
<p className="text-2xl font-bold mt-2">{value}</p>
{trend && (
<span className={trend > 0 ? 'text-green-500' : 'text-red-500'}>
{trend > 0 ? '+' : ''}{trend}%
</span>
)}
</div>
);
}

Component file structure:

'use client'; // If client component

import React, { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { DashboardCard } from '@/components/finance/DashboardCard';
import type { Account } from '@/types/database.types';

// Types/Interfaces
interface AccountsPageProps {
// Props type definition
}

// Component
export default function AccountsPage({ }: AccountsPageProps) {
// Hooks
const { user } = useAuth();
const [accounts, setAccounts] = useState<Account[]>([]);
const [loading, setLoading] = useState(true);

// Effects
useEffect(() => {
// Effect logic
}, []);

// Event handlers
const handleClick = () => {
// Handler logic
};

// Render helpers
const renderAccount = (account: Account) => {
return <div key={account.id}>{account.account_name}</div>;
};

// Render
if (loading) {
return <LoadingSpinner />;
}

return (
<div>
{accounts.map(renderAccount)}
</div>
);
}

Naming Conventions

Variables:

// camelCase for variables and functions
const userId = user.id;
const accountBalance = 10000;

function getUserAccounts() {}

Components:

// PascalCase for components
function DashboardCard() {}
function UserProfile() {}

Constants:

// SCREAMING_SNAKE_CASE for constants
const MAX_RETRY_ATTEMPTS = 3;
const API_BASE_URL = 'https://api.example.com';

Files:

// Component files: PascalCase.tsx
DashboardCard.tsx
UserProfile.tsx

// Utility files: lowercase or camelCase
supabase.ts
plaid.ts
formatCurrency.ts

// Page files: lowercase
page.tsx
layout.tsx

Code Organization

Keep files focused and small:

// ✅ Good: Single responsibility
// lib/currency.ts
export function formatCurrency(cents: number): string {
// Implementation
}

export function dollarsToCents(dollars: number): number {
// Implementation
}

// ❌ Bad: Kitchen sink file
// lib/utils.ts - contains 50+ unrelated functions

Use barrel exports:

// components/finance/index.ts
export { DashboardCard } from './DashboardCard';
export { PlaidLink } from './PlaidLink';
export { SpendingChart } from './SpendingChart';

// Then import
import { DashboardCard, PlaidLink } from '@/components/finance';

Error Handling

Always handle errors gracefully:

// ✅ Good: Proper error handling
export async function getUserAccounts(userId: string): Promise<Account[]> {
try {
const { data, error } = await supabase
.from('accounts')
.select('*')
.eq('user_id', userId);

if (error) {
console.error('Error fetching accounts:', error);
return [];
}

return data || [];
} catch (error) {
console.error('Error in getUserAccounts:', error);
return [];
}
}

// ❌ Bad: No error handling
export async function getUserAccounts(userId: string): Promise<Account[]> {
const { data } = await supabase
.from('accounts')
.select('*')
.eq('user_id', userId);

return data; // Could be undefined!
}

User-facing error messages:

// ✅ Good: Helpful error message
if (error.includes('expired')) {
toast.error('Your session has expired. Please sign in again.');
router.push('/finance/login');
}

// ❌ Bad: Technical error exposed to user
toast.error(error.message); // "Row violates RLS policy"

Testing

Manual Testing Checklist

Before submitting PR, test:

Authentication:

  • Sign up with invite code
  • Sign in with valid credentials
  • Sign in with invalid credentials
  • Sign out
  • Session persistence after refresh
  • Protected routes redirect to login

Plaid Integration:

  • Connect bank account (sandbox)
  • View connected accounts
  • Sync transactions
  • Unlink account
  • Handle connection errors

Data Display:

  • Dashboard loads correctly
  • Account balances are accurate
  • Transactions display correctly
  • Charts render properly
  • Pagination works

Edge Cases:

  • Empty states (no accounts, no transactions)
  • Loading states
  • Error states
  • Large datasets (100+ items)
  • Mobile responsive

Writing Unit Tests

Test helper functions:

// lib/__tests__/currency.test.ts
import { formatCurrency, dollarsToCents, centsToDollars } from '../currency';

describe('Currency utilities', () => {
describe('formatCurrency', () => {
it('formats positive amounts correctly', () => {
expect(formatCurrency(123456)).toBe('$1,234.56');
});

it('formats negative amounts correctly', () => {
expect(formatCurrency(-5000)).toBe('-$50.00');
});

it('formats zero correctly', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
});

describe('dollarsToCents', () => {
it('converts dollars to cents', () => {
expect(dollarsToCents(19.99)).toBe(1999);
expect(dollarsToCents(100)).toBe(10000);
});

it('handles floating point precision', () => {
expect(dollarsToCents(0.1 + 0.2)).toBe(30); // Not 29 or 31
});
});
});

Testing with Plaid Sandbox

Test credentials:

// Successful connection
username: 'user_good'
password: 'pass_good'

// Multi-factor auth
username: 'user_good' (will prompt for MFA code: 1234)

// Invalid credentials
username: 'user_bad'
password: 'pass_bad'

// Locked account
username: 'user_locked'
password: 'pass_locked'

Code Review

Reviewer Checklist

When reviewing code:

Functionality:

  • Code does what PR description says
  • No obvious bugs
  • Edge cases handled
  • Error handling present

Code Quality:

  • Follows TypeScript best practices
  • No any types (unless documented why)
  • Functions are focused and small
  • Variable names are clear
  • No commented-out code

Performance:

  • No N+1 queries
  • Pagination for large datasets
  • Proper indexes used
  • No unnecessary re-renders

Security:

  • No secrets in code
  • Input validation present
  • Authorization checks present
  • SQL injection prevented

Testing:

  • Tested manually
  • Tests added for new features
  • No breaking changes to existing tests

Giving Feedback

Be constructive and specific:

Good:

Consider using `useMemo` here to avoid recalculating on every render:

const sortedAccounts = useMemo(
() => accounts.sort((a, b) => b.balance - a.balance),
[accounts]
);

Bad:

This is slow.

Ask questions:

Good:

Why did you choose to use a separate API call here instead of
including this in the initial query? Is there a performance reason?

Acknowledge good code:

Good:

Nice refactor! This is much more readable than the previous version.

Deployment

Pre-Deployment Checklist

Before deploying to production:

  • All tests pass
  • Code reviewed and approved
  • Environment variables set in Vercel
  • Database migrations run
  • No console.log statements (or only intentional logging)
  • Error handling in place
  • Performance tested
  • Security review completed
  • Documentation updated

Deployment Process

1. Merge to main:

# After PR approval
git checkout main
git pull origin main
# Squash and merge PR on GitHub

2. Vercel auto-deploys:

  • Vercel detects push to main
  • Runs build
  • Deploys to production
  • Assigns deployment URL

3. Verify deployment:

  • Check deployment URL
  • Test critical flows
  • Monitor for errors

4. Rollback if needed:

# In Vercel dashboard
# Go to Deployments
# Find previous stable deployment
# Click "Promote to Production"

Environment-Specific Config

Development:

PLAID_ENV=sandbox
# Use test credentials

Staging:

PLAID_ENV=development
# Use real credentials but Plaid development mode

Production:

PLAID_ENV=production
# Real credentials, real bank data

Daily Workflow

Morning:

# Update local repo
git checkout main
git pull origin main

# Check for updates
npm install

# Start dev server
npm run dev

During Development:

# Make changes
# Test changes
# Commit frequently

git add .
git commit -m "feat: add feature X"

End of Day:

# Push work in progress
git push origin feature/your-feature

# Or create draft PR for early feedback

Weekly:

# Update dependencies
npm update

# Check for security issues
npm audit
npm audit fix

Best Practices Summary

Do:

  • Write TypeScript, not JavaScript
  • Use helper functions from lib/
  • Handle errors gracefully
  • Add loading states
  • Validate user input
  • Write descriptive commit messages
  • Test before submitting PR
  • Review others' code
  • Update documentation

Don't:

  • Commit secrets or API keys
  • Use any type without good reason
  • Skip error handling
  • Write god files (1000+ lines)
  • Ignore TypeScript errors
  • Deploy without testing
  • Leave console.logs in production code
  • Skip code review

Next Steps: