Table of Contents
Overview
The NVLP Client Library provides a unified TypeScript interface for interacting with the NVLP API. It combines PostgREST database access with Edge Function API calls, offering automatic authentication, retry logic, and comprehensive type safety.
🎯 Unified Interface
Single client for both PostgREST (database) and Edge Functions (API) with consistent patterns.
🔐 Automatic Authentication
Built-in JWT token management, refresh handling, and session state management.
🔄 Retry Logic
Automatic retry with exponential backoff for failed requests and network issues.
📝 Full Type Safety
Complete TypeScript support with database schema types and runtime validation.
⚡ Performance Optimized
Optimized for both direct database access and complex API operations.
📱 Mobile Ready
Built-in offline queue support and mobile-optimized patterns.
Installation
# Install in the workspace
pnpm install @nvlp/client
# Or install in a specific package
pnpm --filter your-app add @nvlp/client
Quick Start
Unified Client (Recommended)
The NVLPClient is the recommended approach for all NVLP interactions. It provides a single interface for both PostgREST and Edge Functions.
import { createNVLPClient, SessionProvider } from '@nvlp/client';
import { createClient } from '@supabase/supabase-js';
// Create session provider (integrate with your auth system)
class MySessionProvider implements SessionProvider {
constructor(private supabaseClient: any) {}
async getSession() {
const { data: { session } } = await this.supabaseClient.auth.getSession();
return session;
}
async ensureValidSession() {
const { data: { session }, error } = await this.supabaseClient.auth.getSession();
if (!session) {
throw new Error('No active session');
}
// Check if token needs refresh
if (session.expires_at && session.expires_at * 1000 < Date.now()) {
const { data: refreshData, error: refreshError } =
await this.supabaseClient.auth.refreshSession();
if (refreshError || !refreshData.session) {
throw new Error('Failed to refresh session');
}
return refreshData.session;
}
return session;
}
onSessionChange(handler: (session: any) => void) {
const { data: { subscription } } = this.supabaseClient.auth.onAuthStateChange(
(event, session) => handler(session)
);
return () => subscription.unsubscribe();
}
}
// Create unified NVLP client
const supabaseClient = createClient('https://your-project.supabase.co', 'your-anon-key');
const sessionProvider = new MySessionProvider(supabaseClient);
const nvlp = createNVLPClient({
supabaseUrl: 'https://your-project.supabase.co',
supabaseAnonKey: 'your-anon-key',
customDomain: 'https://api.yourdomain.com', // Optional
sessionProvider,
});
// Use PostgREST for direct database access
const budgets = await nvlp.budgets
.eq('is_active', true)
.order('created_at', false)
.get();
// Use Edge Functions for complex operations
const dashboard = await nvlp.get(`/budgets/${budgetId}/dashboard`);
const newTransaction = await nvlp.post(`/budgets/${budgetId}/transactions`, transactionData);
Simple Setup from Environment
import { createNVLPClientFromEnv } from '@nvlp/client';
// Automatically uses SUPABASE_URL and SUPABASE_ANON_KEY environment variables
const nvlp = createNVLPClientFromEnv({
customDomain: 'https://api.yourdomain.com',
});
// Add authentication later
nvlp.setSessionProvider(sessionProvider);
Authenticated PostgREST Client
For applications that only need PostgREST database access with authentication:
import { createAuthenticatedPostgRESTClient, SessionProvider } from '@nvlp/client';
// Implement session provider
const sessionProvider: SessionProvider = {
getSession: () => yourAuthSystem.getSession(),
ensureValidSession: () => yourAuthSystem.ensureValidSession(),
onSessionChange: (handler) => yourAuthSystem.onSessionChange(handler),
};
// Create authenticated client
const authClient = createAuthenticatedPostgRESTClient(
{ supabaseUrl: '...', supabaseAnonKey: '...' },
sessionProvider
);
// Use convenience methods (automatic auth handling)
const budgets = await authClient.budgets.list();
const categories = await authClient.categories.listByBudget(budgetId);
Query Builder API
The PostgREST client provides a fluent query builder interface for database operations:
Filtering
// Basic filters
client.budgets
.eq('is_active', true) // WHERE is_active = true
.neq('user_id', 'some-id') // WHERE user_id != 'some-id'
.gt('available_amount', 0) // WHERE available_amount > 0
.gte('created_at', '2025-01-01') // WHERE created_at >= '2025-01-01'
.lt('updated_at', '2025-12-31') // WHERE updated_at < '2025-12-31'
.lte('available_amount', 1000) // WHERE available_amount <= 1000
.like('name', '*Budget*') // WHERE name LIKE '%Budget%'
.ilike('description', '*test*') // WHERE description ILIKE '%test%'
.in('id', ['id1', 'id2', 'id3']) // WHERE id IN ('id1', 'id2', 'id3')
.isNull('description') // WHERE description IS NULL
.isNotNull('description') // WHERE description IS NOT NULL
// Complex filters
.or('name.eq.Budget1,name.eq.Budget2') // WHERE (name = 'Budget1' OR name = 'Budget2')
.and('is_active.eq.true,user_id.eq.123') // WHERE (is_active = true AND user_id = '123')
Ordering and Pagination
client.budgets
.order('created_at', false) // ORDER BY created_at DESC
.order('name', true) // ORDER BY name ASC
.limit(10) // LIMIT 10
.offset(20) // OFFSET 20
.range(0, 9) // Range header: 0-9
Selection and Relationships
// Select specific columns
client.budgets
.select('id,name,created_at')
// Select with relationships
client.categories
.select('*,envelopes(*)') // Include related envelopes
// Complex relationship queries
client.transactions
.select(`
*,
from_envelope:envelopes!from_envelope_id(name),
to_envelope:envelopes!to_envelope_id(name),
payee:payees(name),
income_source:income_sources(name)
`)
Execution Methods
// Query execution
const results = await client.budgets.get(); // Execute and return array
const single = await client.budgets.single(); // Execute and return single record
// CRUD operations
const created = await client.budgets.post(data); // INSERT
const updated = await client.budgets.patch(data); // UPDATE
const deleted = await client.budgets.delete(); // DELETE
Convenience Methods
The authenticated client provides convenient methods for common operations across all entity types:
Budgets
// Budget operations
await authClient.budgets.list() // List user's budgets
await authClient.budgets.get(id) // Get budget by ID
await authClient.budgets.create(data) // Create budget
await authClient.budgets.update(id, data) // Update budget
await authClient.budgets.delete(id) // Delete budget
Categories
// Category operations
await authClient.categories.listByBudget(budgetId) // List categories for budget
await authClient.categories.getTree(budgetId) // Get hierarchical categories
await authClient.categories.get(id) // Get category by ID
await authClient.categories.create(data) // Create category
await authClient.categories.update(id, data) // Update category
await authClient.categories.delete(id) // Delete category
Envelopes
// Envelope operations
await authClient.envelopes.listByBudget(budgetId) // List envelopes for budget
await authClient.envelopes.getNegativeBalance(budgetId) // Get negative balance envelopes
await authClient.envelopes.getByType(budgetId, type) // Get envelopes by type
await authClient.envelopes.get(id) // Get envelope by ID
await authClient.envelopes.create(data) // Create envelope
await authClient.envelopes.update(id, data) // Update envelope
await authClient.envelopes.delete(id) // Delete envelope
Transactions
// Transaction operations
await authClient.transactions.listByBudget(budgetId, limit) // List transactions for budget
await authClient.transactions.get(id) // Get transaction with details
await authClient.transactions.getByEnvelope(envelopeId) // Get envelope transactions
await authClient.transactions.getByPayee(payeeId) // Get payee transactions
await authClient.transactions.getUncleared(budgetId) // Get uncleared transactions
await authClient.transactions.create(data) // Create transaction
await authClient.transactions.update(id, data) // Update transaction
await authClient.transactions.softDelete(id) // Soft delete transaction
await authClient.transactions.restore(id) // Restore transaction
⚠️ Transaction Creation
For production applications, use Edge Functions for transaction creation to ensure proper validation and balance updates. PostgREST should primarily be used for querying transaction data.
Session Management
The client library integrates with your authentication system through the SessionProvider interface:
interface SessionProvider {
getSession(): Promise;
ensureValidSession(): Promise;
onSessionChange(handler: (session: Session | null) => void): () => void;
}
// Example implementation with Supabase Auth
class SupabaseSessionProvider implements SessionProvider {
constructor(private supabaseClient: SupabaseClient) {}
async getSession() {
const { data: { session } } = await this.supabaseClient.auth.getSession();
return session;
}
async ensureValidSession() {
const session = await this.getSession();
if (!session) {
throw new Error('No active session');
}
// Auto-refresh if needed
if (this.isTokenExpired(session)) {
const { data, error } = await this.supabaseClient.auth.refreshSession();
if (error || !data.session) {
throw new Error('Failed to refresh session');
}
return data.session;
}
return session;
}
onSessionChange(handler: (session: Session | null) => void) {
const { data: { subscription } } = this.supabaseClient.auth.onAuthStateChange(
(event, session) => handler(session)
);
return () => subscription.unsubscribe();
}
private isTokenExpired(session: Session): boolean {
return session.expires_at ? session.expires_at * 1000 < Date.now() : false;
}
}
Error Handling
The client provides comprehensive error handling with specific error types:
import { HttpError, NetworkError, TimeoutError } from '@nvlp/client';
try {
const budget = await client.budgets.eq('id', 'invalid-id').single();
} catch (error) {
if (error instanceof HttpError) {
switch (error.status) {
case 401:
// Handle authentication error
console.log('Authentication required');
break;
case 403:
// Handle authorization error (RLS)
console.log('Access denied');
break;
case 404:
// Handle not found
console.log('Budget not found');
break;
default:
console.log('HTTP error:', error.message);
}
} else if (error instanceof NetworkError) {
// Handle network issues
console.log('Network error, retrying...');
} else if (error instanceof TimeoutError) {
// Handle timeouts
console.log('Request timed out');
} else {
// Handle other errors
console.log('Unexpected error:', error);
}
}
Integration Examples
React Integration
// Custom hook for NVLP client
import { createNVLPClient } from '@nvlp/client';
import { useAuth } from './auth-context';
function useNVLP() {
const { sessionProvider } = useAuth();
const nvlp = useMemo(() => createNVLPClient({
supabaseUrl: process.env.REACT_APP_SUPABASE_URL,
supabaseAnonKey: process.env.REACT_APP_SUPABASE_ANON_KEY,
sessionProvider,
}), [sessionProvider]);
return nvlp;
}
// Component using the client
function BudgetDashboard({ budgetId }: { budgetId: string }) {
const [budgetData, setBudgetData] = useState(null);
const nvlp = useNVLP();
useEffect(() => {
async function loadBudgetData() {
try {
const [budget, categories, envelopes, recentTransactions] = await Promise.all([
nvlp.budgets.eq('id', budgetId).single(),
nvlp.categories.eq('budget_id', budgetId).get(),
nvlp.envelopes.eq('budget_id', budgetId).eq('is_active', true).get(),
nvlp.transactions.eq('budget_id', budgetId).order('created_at', false).limit(10).get(),
]);
setBudgetData({
budget,
categories,
envelopes,
recentTransactions,
});
} catch (error) {
console.error('Failed to load budget data:', error);
}
}
loadBudgetData();
}, [budgetId, nvlp]);
if (!budgetData) return Loading...;
return (
{budgetData.budget.name}
Available: ${budgetData.budget.available_amount}
Envelopes
{budgetData.envelopes.map(envelope => (
{envelope.name}: ${envelope.current_balance}
))}
Recent Transactions
{budgetData.recentTransactions.map(transaction => (
{transaction.description}: ${transaction.amount}
))}
);
}
React Native Integration
// React Native setup with AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createNVLPClient, AsyncStorageImpl } from '@nvlp/client';
const nvlp = createNVLPClient({
supabaseUrl: 'https://your-project.supabase.co',
supabaseAnonKey: 'your-anon-key',
sessionProvider: new ReactNativeSessionProvider(),
offlineStorage: new AsyncStorageImpl(AsyncStorage),
offlineQueue: {
enabled: true,
maxRetries: 3,
retryDelay: 1000,
},
});
// Service class for budget operations
class BudgetService {
constructor(private nvlp: NVLPClient) {}
async createQuickExpense(budgetId: string, expense: {
amount: number;
description: string;
envelopeId: string;
payeeId: string;
}) {
// Use Edge Function for complex validation
return this.nvlp.post(`/budgets/${budgetId}/transactions`, {
transaction_type: 'expense',
amount: expense.amount,
description: expense.description,
from_envelope_id: expense.envelopeId,
payee_id: expense.payeeId,
transaction_date: new Date().toISOString().split('T')[0],
});
}
async getEnvelopeBalance(envelopeId: string) {
const envelope = await this.nvlp.envelopes.eq('id', envelopeId).single();
return envelope.current_balance;
}
}
Best Practices
📋 When to Use PostgREST vs Edge Functions
Use PostgREST for:
- Simple CRUD operations
- Data querying and filtering
- Bulk operations
- Read-heavy operations
Use Edge Functions for:
- Complex transaction creation with validation
- Business logic enforcement
- Multi-table operations requiring database transactions
- Balance calculations and updates
- Complex aggregations and reporting
Performance Tips
- Use specific selects: Only select the columns you need to reduce payload size
- Implement pagination: Use
limit()
andoffset()
for large datasets - Batch operations: Use
Promise.all()
for independent parallel requests - Cache responses: Implement client-side caching for frequently accessed data
- Use relationships wisely: Embed related data when needed, but avoid deep nesting
Security Best Practices
- Always use authenticated clients for user-specific operations
- Validate data on the client before sending to reduce server load
- Handle token refresh automatically to maintain seamless user experience
- Use HTTPS only in production environments
- Implement proper error boundaries to handle authentication failures gracefully
Type Safety
import type { Database } from '@nvlp/types';
// All database types are available
type Budget = Database['public']['Tables']['budgets']['Row'];
type NewBudget = Database['public']['Tables']['budgets']['Insert'];
type UpdateBudget = Database['public']['Tables']['budgets']['Update'];
// Use types in your functions
async function createBudget(data: NewBudget): Promise {
return authClient.budgets.create(data);
}
async function updateBudget(id: string, data: UpdateBudget): Promise {
return authClient.budgets.update(id, data);
}
🚀 Next Steps
Ready to integrate NVLP into your application? Check out these resources:
- PostgREST Integration Guide - Direct database access patterns
- Edge Functions Guide - Complex business logic implementation
- Authentication Guide - Complete auth setup and magic links
- API Reference - Complete endpoint documentation