npm package: @sygnl/supabase-cf
Description: Lightweight Supabase REST API client for Cloudflare Workers
Author: Edge Foundry, inc.
Latest version: 1.0.3

# @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

```bash
npm install @sygnl/supabase-cf
```

## Quick Start

```typescript
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:

```bash
# 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

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
.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

```typescript
// 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

```typescript
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`](./examples/worker-example.ts) for a complete Cloudflare Worker implementation.

```typescript
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`](./examples/index.html) for a complete test interface with Insert/Read operations.

**Setup for HTML example:**

1. **Add assets binding to `wrangler.toml`:**
```toml
[assets]
directory = "./public"
binding = "ASSETS"
```

2. **Serve the HTML:**
```typescript
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...
  }
};
```

3. **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

```typescript
// ❌ 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

```typescript
// 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:

```bash
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](https://Sygnl.app)