Skip to main content
Deriverse uses Supabase as its database backend, providing PostgreSQL persistence, Row Level Security (RLS), and real-time subscriptions. The database stores wallet metadata, cached trade data, and user annotations.

Database Provider

Supabase

Version: @supabase/supabase-js v2.95.3PostgreSQL database with authentication, real-time subscriptions, and automatic API generation.

Connection Setup

The Supabase client is initialized in src/lib/supabaseClient.ts:
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);
Ensure environment variables are set before accessing the database. Missing credentials will cause runtime errors.

Data Sources

Deriverse pulls data from three sources:
1

Mock Data

MOCK_TRADES from mockData.ts for demo and testing. No database connection required.
2

Live Blockchain Data

Fetched via HeliusService and DeriverseTradeService from Solana devnet/mainnet.
3

Persistent Storage

Cached in Supabase for fast loading, offline analytics, and cross-device sync.

Table: user_wallets

Purpose: Track Solana wallet addresses that have been looked up, with metadata about connection method, network, and sync status.

Schema Definition

CREATE TABLE user_wallets (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  wallet_address TEXT UNIQUE NOT NULL,
  network TEXT NOT NULL,
  wallet_provider TEXT NOT NULL DEFAULT 'manual',
  connection_method TEXT NOT NULL DEFAULT 'manual',
  last_synced_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_user_wallets_address ON user_wallets(wallet_address);
CREATE INDEX idx_user_wallets_last_synced ON user_wallets(last_synced_at DESC);

Field Descriptions

FieldTypeNullableDefaultDescription
idUUIDNogen_random_uuid()Primary key, auto-generated
wallet_addressTEXTNo-Full Solana public key (44 characters)
networkTEXTNo-Solana network: 'devnet', 'mainnet-beta', or 'mock'
wallet_providerTEXTNo'manual'Wallet app: 'Phantom', 'Solflare', or 'manual'
connection_methodTEXTNo'manual'How address was obtained: 'manual' or 'wallet_connect'
last_synced_atTIMESTAMPTZYesNULLWhen trades were last fetched from blockchain
created_atTIMESTAMPTZNoNOW()When this wallet was first added
updated_atTIMESTAMPTZNoNOW()When any field was last modified

Design Decisions

  • Avoids NULL complexity in queries and application code
  • 'manual' is a sensible default for manually entered addresses
  • Overridden to actual wallet name (e.g., 'Phantom') when using wallet connect
  • Reduces need for null checks in TypeScript
  • NULL explicitly means “never synced” (wallet saved but trades not fetched yet)
  • Allows saving wallet address before running blockchain lookup
  • Used to show “Last updated X hours ago” or “Never synced” in UI
  • updated_at: Database metadata (when ANY field changed)
  • last_synced_at: Business logic (when blockchain data was fetched)
  • Serves different purposes: audit trail vs data freshness

Example Records

{
  "wallet_address": "7xKXtG2JtPZy4WqU9emKfbeuSTePDnSqeAACqVNx3k8s",
  "network": "devnet",
  "wallet_provider": "manual",
  "connection_method": "manual",
  "last_synced_at": "2026-02-13T17:30:00+05:30",
  "created_at": "2026-02-13T17:30:00+05:30",
  "updated_at": "2026-02-13T17:30:00+05:30"
}

Table: trades

Purpose: Cache blockchain trade data from Deriverse for fast loading, offline analytics, and historical preservation.

Schema Definition

CREATE TABLE trades (
  id TEXT PRIMARY KEY,
  wallet_address TEXT NOT NULL REFERENCES user_wallets(wallet_address) ON DELETE CASCADE,
  symbol TEXT NOT NULL,
  quote_currency TEXT NOT NULL,
  side TEXT NOT NULL,
  order_type TEXT NOT NULL,
  quantity NUMERIC NOT NULL,
  price NUMERIC NOT NULL,
  notional NUMERIC NOT NULL,
  pnl NUMERIC NOT NULL,
  fee NUMERIC NOT NULL,
  fee_currency TEXT NOT NULL,
  opened_at TIMESTAMPTZ NOT NULL,
  closed_at TIMESTAMPTZ NOT NULL,
  duration_seconds INTEGER NOT NULL,
  is_win BOOLEAN NOT NULL,
  tx_signature TEXT NOT NULL,
  is_maker BOOLEAN,
  leverage NUMERIC,
  liquidation_price NUMERIC,
  margin_used NUMERIC,
  fee_breakdown JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_trades_wallet ON trades(wallet_address);
CREATE INDEX idx_trades_opened_at ON trades(opened_at DESC);
CREATE INDEX idx_trades_symbol ON trades(symbol);

Key Fields

Trade Identity

id: Unique trade identifiertx_signature: Blockchain transaction hash

Trade Details

symbol, side, order_typequantity, price, notional

Performance

pnl: Profit and lossis_win: Boolean success flagfee, fee_breakdown

Storage Estimates

Per Trade: ~420 bytes (350 bytes data + 70 bytes indexes) Supabase Free Tier (500 MB):
TradesStorage% Used
1,000420 KB0.08%
10,0004.2 MB0.84%
100,00042 MB8.4%
1,000,000420 MB84%
Index overhead is ~20% but provides 100x faster queries. Trade-off: 2-3ms slower inserts for sub-millisecond lookups.

NUMERIC vs FLOAT

Financial Precision
  • Exact decimal representation (0.1 + 0.2 = 0.3) ✅
  • No floating-point rounding errors
  • Industry standard for financial data
  • Handles crypto decimals (e.g., 0.000000000003)

Table: trade_annotations

Purpose: Store user journal entries, tags, and lessons learned for each trade. Migrated from localStorage for cross-device sync.

Schema (Planned)

CREATE TABLE trade_annotations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  wallet_address TEXT NOT NULL REFERENCES user_wallets(wallet_address) ON DELETE CASCADE,
  trade_id TEXT NOT NULL REFERENCES trades(id) ON DELETE CASCADE,
  note TEXT,
  tags TEXT[],
  lessons_learned TEXT,
  emotional_state TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE(wallet_address, trade_id)
);
Annotations currently use localStorage as a fallback, synced to Supabase when online. Full migration planned for future release.

Data Flow

Wallet Lookup Flow

The application follows this sequence when loading trade data:

Caching Strategy

Deriverse implements a time-based caching strategy to balance data freshness with RPC efficiency:
1

Fresh Data (< 24 hours)

Load directly from Supabase cache. No blockchain query needed.
2

Stale Data (> 24 hours)

Display cached data with a “Refresh” button to fetch updated trades.
3

Never Synced (NULL)

Auto-fetch from blockchain on first load. User wallet is saved immediately.
The 24-hour threshold is configurable in SupabaseWalletService.isDataStale(wallet, hoursThreshold).

Row Level Security (RLS)

Policy Configuration

Supabase RLS ensures users can only access their own data:
-- Policy: Users can read their own wallet data
CREATE POLICY "Users can view their own wallets"
ON user_wallets
FOR SELECT
USING (wallet_address = current_setting('request.jwt.claim.wallet_address'));

-- Policy: Users can update their own wallet sync time
CREATE POLICY "Users can update their own wallets"
ON user_wallets
FOR UPDATE
USING (wallet_address = current_setting('request.jwt.claim.wallet_address'));

-- Policy: Users can read their own trades
CREATE POLICY "Users can view their own trades"
ON trades
FOR SELECT
USING (wallet_address = current_setting('request.jwt.claim.wallet_address'));
RLS policies are enforced at the database level. Even if the frontend is compromised, users cannot access other users’ data.

Service Layer

SupabaseWalletService

Core methods for wallet management:
src/services/SupabaseWalletService.ts
export class SupabaseWalletService {
  // Save or update wallet (upsert)
  async saveWallet(params: SaveWalletParams): Promise<UserWallet>

  // Get wallet by address
  async getWallet(address: string): Promise<UserWallet | null>

  // Update last_synced_at timestamp
  async updateSyncTime(address: string): Promise<void>

  // Get recently synced wallets
  async getRecentWallets(limit: number = 5): Promise<UserWallet[]>

  // Check if data is stale
  isDataStale(wallet: UserWallet, hoursThreshold: number = 24): boolean

  // Get human-readable time since sync
  getTimeSinceSync(wallet: UserWallet): string
}

SupabaseTradeService

Manages trade data persistence:
src/services/SupabaseTradeService.ts
export class SupabaseTradeService {
  // Save trades (upsert, idempotent)
  async saveTrades(walletAddress: string, trades: Trade[]): Promise<void>

  // Get trades for wallet
  async getTrades(walletAddress: string): Promise<Trade[]>

  // Get trades with filters (symbol, date range, side)
  async getFilteredTrades(filters: TradeFilters): Promise<Trade[]>
}
All save operations use upsert to ensure idempotency. Calling saveTrades() multiple times with the same data is safe.

Client-Side Storage

localStorage Fallback

Annotations use localStorage as an offline-first fallback:
src/lib/annotationStorage.ts
export interface Annotation {
  tradeId: string;
  note: string;
  tags: string[];
  lessonsLearned: string;
  timestamp: string;
}

// Save annotation locally
export function saveAnnotation(annotation: Annotation): void;

// Load all annotations
export function loadAnnotations(): Annotation[];

// Sync to Supabase when online
export function syncAnnotations(): Promise<void>;
Annotations automatically sync to Supabase when the user goes online. Use the SyncStatus component to show sync state.

Migration Notes

Current State (February 2026)

Implemented

  • user_wallets table with RLS
  • trades caching with 24-hour strategy
  • Wallet lookup and sync
  • Service layer abstractions

Planned

  • trade_annotations full migration
  • Real-time subscriptions
  • Pre-calculated analytics tables
  • SWR/TanStack Query integration

Backward Compatibility

  • Mock Mode: Works without database connection
  • Lookup Feature: Functions without Supabase (slower, no caching)
  • Annotations: localStorage fallback prevents data loss
The database is additive-only. No breaking changes have been introduced since initial launch.

Performance Optimizations

Current Optimizations

  1. Indexes: wallet_address, last_synced_at, opened_at, symbol
  2. Caching: 24-hour window prevents redundant blockchain queries
  3. Upsert Logic: Prevents duplicate entries and race conditions
  4. JSONB Fields: Efficient storage for fee_breakdown with GIN indexing

Future Enhancements

Use Supabase real-time to push live trade updates to connected clients without polling.
Pre-calculate win rate, total PnL, and average trade duration for instant analytics.
Client-side caching with useSWR or TanStack Query for optimistic updates.
Fetch only new trades since last_synced_at instead of full wallet history.

Last Updated: February 2026 For architecture details, see Architecture. For deployment setup, see Deployment.

Build docs developers (and LLMs) love