Development Workflow
Best practices and workflow for OneLibro development.
Table of Contents
Getting Started
Initial Setup
- Clone repository:
git clone https://github.com/Yatheesh-Nagella/yatheesh-portfolio.git
cd yatheesh-portfolio
- Install dependencies:
npm install
- Set up environment variables:
cp .env.example .env
# Edit .env with your actual credentials
- Run development server:
npm run dev
- Open in browser:
- Main: http://localhost:3000
- Finance: http://finance.localhost:3000
- Admin: http://admin.localhost:3000
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_categoriestable - 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 featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, no logic change)refactor: Code refactoring (no feature or bug change)perf: Performance improvementstest: Adding or updating testschore: 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
- Create pull request:
# Push your branch
git push origin feature/your-feature-name
# Create PR on GitHub
# Use PR template if available
- PR Title Format:
feat(scope): Brief description of changes
- 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
- Before merging:
- ✅ All CI checks pass
- ✅ Code reviewed and approved
- ✅ No merge conflicts
- ✅ Branch is up to date with main
- 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
anytypes (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
anytype 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:
- Review Architecture Overview for system design
- Read Troubleshooting Guide for common issues