NVLP Client Library Usage Guide

Complete guide to using the NVLP unified client library for seamless API integration with automatic authentication, type safety, and performance optimization.

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

Security Best Practices

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: