Skip to main content

Overview

The @trendingsociety/db package provides a type-safe Supabase client with auto-generated types from the database schema.
Types are auto-generated from the 127-table schema. Run pnpm db:generate-types after schema changes.

Installation

pnpm add @trendingsociety/db

Quick Start

import { createClient, createServiceClient } from '@trendingsociety/db';

// Client-side (respects RLS)
const supabase = createClient();

// Server-side (bypasses RLS)
const adminSupabase = createServiceClient();

API Reference

createClient()

Creates a Supabase client for client-side use. Respects RLS policies.
import { createClient } from '@trendingsociety/db';

const supabase = createClient();

const { data, error } = await supabase
  .from('content')
  .select('*');

createServiceClient()

Creates a Supabase client with service role. Bypasses all RLS.
Only use server-side. Never expose to client. Never import in client components.
import { createServiceClient } from '@trendingsociety/db';

const supabase = createServiceClient();

// Can access all data, any tenant
const { data } = await supabase
  .from('system.api_keys')
  .select('*');

createServerClient(cookieStore)

Creates a server-side client for Next.js App Router.
import { createServerClient } from '@trendingsociety/db';
import { cookies } from 'next/headers';

export async function getData() {
  const cookieStore = await cookies();
  const supabase = createServerClient(cookieStore);

  const { data } = await supabase
    .from('content')
    .select('*');

  return data;
}

Type Exports

// Generated from schema
import type { Database } from '@trendingsociety/db';
import type { Tables, TablesInsert, TablesUpdate } from '@trendingsociety/db';

// Specific table types
type Content = Tables<'content'>;
type ContentInsert = TablesInsert<'content'>;
type ContentUpdate = TablesUpdate<'content'>;

Query Patterns

Basic Select

const { data, error } = await supabase
  .from('content')
  .select('*')
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .limit(10);

With Relations

const { data } = await supabase
  .from('publisher_posts')
  .select(`
    id,
    title,
    vertical:publisher_verticals(name, slug),
    analytics:publisher_post_analytics(page_views, revenue)
  `)
  .eq('status', 'published');

Insert

const { data, error } = await supabase
  .from('content')
  .insert({
    title: 'New Article',
    slug: 'new-article',
    tenant_id: tenantId // Required for tenant-scoped tables
  })
  .select()
  .single();

Upsert

const { data, error } = await supabase
  .from('content')
  .upsert({
    id: existingId, // If exists, update; otherwise insert
    title: 'Updated Title'
  })
  .select()
  .single();

RPC (Stored Functions)

const { data, error } = await supabase.rpc('calculate_metrics', {
  p_tenant_id: tenantId,
  p_start_date: '2025-01-01'
});

Multi-Tenant Queries

Always ensure tenant context is set. RLS policies filter data automatically, but explicit tenant_id is safer.
// Recommended: explicit tenant filter
const { data } = await supabase
  .from('content')
  .select('*')
  .eq('tenant_id', tenantId);

// Also works: RLS handles it (but less explicit)
const { data: rlsFiltered } = await supabase
  .from('content')
  .select('*');

Schema Access

Access different schemas:
// Public schema (default)
supabase.from('content').select()

// System schema (service role only)
supabase.schema('system').from('api_keys').select()

// Events schema (service role only)
supabase.schema('events').from('outbox').select()

Type Generation

Regenerate types after schema changes:
pnpm --filter @trendingsociety/db generate-types
# or
pnpm db:generate-types
This updates packages/db/src/types/database.types.ts.