JSPM

@sygnl/supabase-cf

1.0.3
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q24625F
  • License Apache-2.0

Lightweight Supabase REST API client for Cloudflare Workers

Package Exports

  • @sygnl/supabase-cf

Readme

@sygnl/supabase-cf

Lightweight Supabase REST API client for Cloudflare Workers. Zero dependencies, fully typed, optimized for edge runtime.

Features

  • Zero Dependencies - Uses native fetch() only
  • Cloudflare Workers First - Optimized for edge runtime
  • Fully Typed - Complete TypeScript support
  • Lightweight - < 5KB gzipped
  • Simple API - Chainable, intuitive methods
  • Fail-Safe - Graceful error handling

Installation

npm install @sygnl/supabase-cf

Quick Start

import { SupabaseClient } from '@sygnl/supabase-cf';

// Initialize client
const supabase = new SupabaseClient({
  url: env.SUPABASE_URL,           // https://xxx.supabase.co
  serviceKey: env.SUPABASE_SERVICE_KEY
});

// Insert data
await supabase.from('events').insert({
  event_id: 'evt_123',
  event_type: 'PURCHASE',
  user_id: 'user_456'
});

// Query data
const { data, error } = await supabase
  .from('events')
  .select('*')
  .eq('user_id', 'user_456')
  .limit(10)
  .execute();

Environment Variables

Set these in your Cloudflare Worker:

# Supabase project URL (plain text Variable, not Secret)
SUPABASE_URL=https://your-project.supabase.co

# Supabase service role key (encrypted Secret)
SUPABASE_SERVICE_KEY=eyJhbG...

Important: Set SUPABASE_URL as a plain text Variable, not an encrypted Secret. URLs don't need encryption.

API Reference

Client Configuration

const supabase = new SupabaseClient({
  url: string,              // Required: Supabase project URL
  serviceKey: string,       // Required: Service role key
  timeout?: number,         // Optional: Request timeout (default: 10000ms)
  retries?: number,         // Optional: Retry attempts (default: 0)
  headers?: Record<string, string>  // Optional: Custom headers
});

Insert

// Single row
await supabase.from('events').insert({
  event_id: 'evt_123',
  event_type: 'PURCHASE'
});

// Multiple rows
await supabase.from('events').insert([
  { event_id: 'evt_123', event_type: 'PURCHASE' },
  { event_id: 'evt_124', event_type: 'PAGE_VIEW' }
]);

Query

// Select all columns
const { data } = await supabase
  .from('events')
  .select()
  .execute();

// Select specific columns
const { data } = await supabase
  .from('events')
  .select('event_id, event_type, created_at')
  .execute();

// Filter
const { data } = await supabase
  .from('events')
  .select()
  .eq('event_type', 'PURCHASE')
  .gte('created_at', '2025-01-01')
  .execute();

// Order and limit
const { data } = await supabase
  .from('events')
  .select()
  .order('created_at', 'desc')
  .limit(100)
  .execute();

// Get single row
const { data, error } = await supabase
  .from('events')
  .select()
  .eq('event_id', 'evt_123')
  .executeSingle();

Upsert

// Upsert with conflict resolution
await supabase.from('events').upsert({
  event_id: 'evt_123',
  event_type: 'PURCHASE',
  updated_at: new Date()
}, {
  onConflict: 'event_id'
});

// Batch upsert
await supabase.from('events').upsert([
  { event_id: 'evt_123', event_type: 'PURCHASE' },
  { event_id: 'evt_124', event_type: 'PAGE_VIEW' }
], {
  onConflict: 'event_id'
});

Filter Operators

.eq('column', value)       // equals
.neq('column', value)      // not equals
.gt('column', value)       // greater than
.gte('column', value)      // greater than or equal
.lt('column', value)       // less than
.lte('column', value)      // less than or equal
.filter('column', 'like', '%pattern%')  // pattern matching
.filter('column', 'in', [1, 2, 3])      // in array

TypeScript Support

// Define your table types
interface Event {
  event_id: string;
  event_type: string;
  user_id: string;
  created_at: string;
}

// Type-safe queries
const supabase = new SupabaseClient({ ... });
const { data } = await supabase
  .from<Event>('events')
  .select()
  .eq('user_id', 'user_456')
  .execute();

// data is typed as Event[]

Error Handling

import { SupabaseHTTPError, SupabaseTimeoutError } from '@sygnl/supabase-cf';

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

if (error) {
  if (error instanceof SupabaseHTTPError) {
    console.error(`HTTP ${error.status}:`, error.body);
  } else if (error instanceof SupabaseTimeoutError) {
    console.error('Request timed out');
  } else {
    console.error('Unknown error:', error);
  }
}

Examples

Basic Worker Example

See examples/worker-example.ts for a complete Cloudflare Worker implementation.

import { SupabaseClient } from '@sygnl/supabase-cf';

interface Env {
  SUPABASE_URL: string;
  SUPABASE_SERVICE_KEY: string;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const supabase = new SupabaseClient({
      url: env.SUPABASE_URL,
      serviceKey: env.SUPABASE_SERVICE_KEY
    });

    // Insert event asynchronously
    ctx.waitUntil(
      supabase.from('events').insert({
        event_id: crypto.randomUUID(),
        event_type: 'PAGE_VIEW',
        timestamp: Date.now()
      })
    );

    return new Response('OK');
  }
};

Interactive HTML Test Interface

See examples/index.html for a complete test interface with Insert/Read operations.

Setup for HTML example:

  1. Add assets binding to wrangler.toml:
[assets]
directory = "./public"
binding = "ASSETS"
  1. Serve the HTML:
export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    
    // Serve static HTML for non-API routes
    if (!url.pathname.startsWith('/api/')) {
      return env.ASSETS.fetch(request);
    }
    
    // Handle API routes...
  }
};
  1. Important - Deployment vs Local Development:
    • Production: Deploy to Cloudflare Workers - works out of the box
    • Local Dev: Requires tunneling (e.g., Cloudflare Tunnel, ngrok) or proxy configuration
    • Proxy Setup: Configure proxy in tsconfig.json or use wrangler dev --remote for testing

Why? The HTML makes requests to /api/* endpoints. Locally, these requests need proper routing which requires either:

  • Deploying to Cloudflare Workers (recommended for testing)
  • Using wrangler dev --remote (runs on Cloudflare's edge)
  • Setting up a local proxy/tunnel

Performance Tips

Batch Operations

// ❌ Slow: Multiple requests
for (const event of events) {
  await supabase.from('events').insert(event);
}

// ✅ Fast: Single batched request
await supabase.from('events').insert(events);

Use waitUntil for Non-Blocking Writes

// Don't block the response
ctx.waitUntil(
  supabase.from('events').insert(data)
);

return new Response('OK'); // Returns immediately

Comparison with Official SDK

Feature @sygnl/supabase-cf @supabase/supabase-js
Size < 5KB ~50KB
Dependencies 0 10+
CF Workers ✅ Native ⚠️ Requires polyfills
TypeScript ✅ Built-in ✅ Built-in
Query Builder
Realtime
Auth
Storage

Use @sygnl/supabase-cf when:

  • Running on Cloudflare Workers
  • Only need database operations
  • Want minimal bundle size
  • Need zero dependencies

Use @supabase/supabase-js when:

  • Need Realtime subscriptions
  • Need Auth features
  • Need Storage features
  • Not size-constrained

Testing

The package includes comprehensive tests covering real-world scenarios:

npm test              # Run tests once
npm run test:watch    # Run tests in watch mode

Test coverage includes:

  • ✅ Configuration validation (service key format, length, JWT structure)
  • ✅ Insert operations (single, batch, error handling)
  • ✅ Select/Query operations (filters, ordering, limits)
  • ✅ Error scenarios (401, 403, 404, 409 with helpful messages)
  • ✅ Edge cases discovered during development

Tests run automatically on every build to ensure reliability.

License

Apache-2.0

Contributing

Contributions welcome! This package is part of the Sygnl SDK