Self-hosted Supabase ships with Edge Functions powered by Deno—a solid choice for lightweight serverless logic. But Edge Functions have real limitations: they run on Deno only, have execution time constraints, and can't access the full npm ecosystem without workarounds. When you need Node.js dependencies, longer-running processes, or integration with existing cloud infrastructure, external compute platforms become necessary.
This guide covers connecting AWS Lambda, Vercel Functions, and Cloudflare Workers to your self-hosted Supabase deployment. The patterns here solve real problems: background job processing, complex business logic, and leveraging platform-specific features while keeping your database self-hosted.
Why External Compute?
Edge Functions work well for simple tasks—validating webhooks, transforming data, sending notifications. But production applications often need more:
Node.js ecosystem access: Libraries like sharp for image processing, puppeteer for PDF generation, or machine learning SDKs that don't run in Deno.
Longer execution times: Edge Functions typically have 30-60 second limits. Background jobs, report generation, or batch processing need minutes, not seconds.
Existing infrastructure integration: Your team already has CI/CD pipelines for Lambda or Vercel. Maintaining separate deployment workflows for Edge Functions adds operational overhead.
Platform-specific features: Vercel's ISR, Cloudflare's D1 bindings, or AWS's tight integration with other services like SQS and S3.
The trade-off is network latency. External functions must traverse the internet to reach your self-hosted instance, while Edge Functions run on the same network. For most use cases, this latency (typically 10-50ms within the same region) is acceptable.
Connection Architecture
Before diving into platform-specific code, understand how external functions connect to self-hosted Supabase:
[External Function] --HTTPS--> [Your Domain / Reverse Proxy] ---> [Kong API Gateway] ---> [PostgREST/GoTrue/Storage]
Your functions connect to the same API endpoint your frontend uses. The critical difference: functions typically use the service_role key to bypass Row Level Security for administrative operations, while frontend code uses the anon key.
Security Considerations
Never expose your service_role key in client-side code. Store it as an environment variable in your serverless platform, and ensure your function code doesn't log or expose it.
Your self-hosted Supabase instance must be reachable from the public internet (or through a VPN/private network if your functions support it). If you've configured a reverse proxy, ensure it properly handles requests from your serverless platform's IP ranges.
Connecting AWS Lambda
AWS Lambda is the most mature serverless platform, with extensive Node.js support and tight integration with other AWS services. Here's how to connect it to self-hosted Supabase.
Lambda Environment Setup
In your Lambda function's configuration, add these environment variables:
SUPABASE_URL=https://supabase.yourdomain.com SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Node.js Lambda Function
// index.mjs
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
export const handler = async (event) => {
try {
// Parse incoming request
const body = JSON.parse(event.body || '{}');
// Example: Create a user record with service role (bypasses RLS)
const { data, error } = await supabase
.from('users')
.insert({ email: body.email, created_at: new Date().toISOString() })
.select()
.single();
if (error) throw error;
return {
statusCode: 200,
body: JSON.stringify({ user: data }),
};
} catch (error) {
console.error('Lambda error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: error.message }),
};
}
};
Connection Pooling for Lambda
Lambda functions scale horizontally—each concurrent invocation creates a new connection to your database. Without pooling, you'll exhaust connections quickly under load.
Self-hosted Supabase includes PgBouncer by default. Use the pooled connection URL (typically port 6543) instead of direct Postgres connections:
// For direct database access (not through PostgREST API)
import postgres from 'postgres';
const sql = postgres(process.env.DATABASE_URL, {
// Lambda best practices
max: 1, // Single connection per instance
idle_timeout: 20,
connect_timeout: 10,
});
See our connection pooling guide for detailed PgBouncer configuration.
Invoking Lambda from Supabase
You can trigger Lambda functions from database changes using database webhooks. Create a webhook that calls your Lambda's API Gateway endpoint:
-- Example: Trigger Lambda on new order
CREATE OR REPLACE FUNCTION notify_lambda()
RETURNS TRIGGER AS $$
BEGIN
PERFORM net.http_post(
url := 'https://your-api-gateway.execute-api.region.amazonaws.com/prod/process-order',
body := jsonb_build_object('order_id', NEW.id, 'amount', NEW.total),
headers := jsonb_build_object('Content-Type', 'application/json')
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Connecting Vercel Functions
Vercel Functions power the API routes in Next.js, Nuxt, and SvelteKit deployments. If your frontend is already on Vercel, adding functions that connect to self-hosted Supabase is straightforward.
Environment Configuration
Add your Supabase credentials in Vercel's dashboard under Project Settings > Environment Variables:
SUPABASE_URL=https://supabase.yourdomain.com SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Next.js API Route Example
// app/api/process-payment/route.ts
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export async function POST(request: Request) {
const body = await request.json();
// Process payment with external service
const paymentResult = await processStripePayment(body.amount);
// Update order in self-hosted Supabase
const { data, error } = await supabase
.from('orders')
.update({
payment_status: 'completed',
payment_id: paymentResult.id
})
.eq('id', body.orderId)
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json({ order: data });
}
Handling Cold Starts
Vercel Functions have cold starts of 500ms to 2 seconds on infrequently-hit routes. Combined with network latency to your self-hosted instance, response times can feel sluggish.
Mitigation strategies:
- Keep functions warm: Use monitoring services to ping critical endpoints regularly
- Reduce bundle size: Only import what you need from
@supabase/supabase-js - Use Vercel Edge Functions for latency-sensitive paths (though they have the same Deno-like limitations as Supabase Edge Functions)
// Lighter import for simple database operations
import { createClient } from '@supabase/supabase-js';
// Instead of importing from '@supabase/supabase-js/dist/module'
Connecting Cloudflare Workers
Cloudflare Workers offer the lowest cold start times (under 5ms) and global edge distribution. They're ideal for latency-sensitive operations that still need self-hosted Supabase.
Worker Setup with Wrangler
Create a wrangler.toml configuration:
name = "supabase-worker" main = "src/index.ts" compatibility_date = "2026-05-01" [vars] SUPABASE_URL = "https://supabase.yourdomain.com" # Store secrets with: wrangler secret put SUPABASE_SERVICE_ROLE_KEY
TypeScript Worker Example
// src/index.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js';
interface Env {
SUPABASE_URL: string;
SUPABASE_SERVICE_ROLE_KEY: string;
}
let supabase: SupabaseClient;
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Reuse client across requests (Workers are long-lived)
if (!supabase) {
supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
}
const url = new URL(request.url);
if (url.pathname === '/api/users' && request.method === 'GET') {
const { data, error } = await supabase
.from('users')
.select('id, email, created_at')
.limit(100);
if (error) {
return Response.json({ error: error.message }, { status: 500 });
}
return Response.json({ users: data });
}
return Response.json({ error: 'Not found' }, { status: 404 });
},
};
Using Hyperdrive for Direct Postgres
Cloudflare Hyperdrive can connect directly to your self-hosted Postgres, bypassing PostgREST entirely. This provides lower latency for complex queries:
// wrangler.toml
[[hyperdrive]]
binding = "DB"
id = "your-hyperdrive-id"
// src/index.ts
import { Client } from 'pg';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const client = new Client(env.DB.connectionString);
await client.connect();
const result = await client.query(
'SELECT * FROM orders WHERE status = $1 LIMIT 50',
['pending']
);
await client.end();
return Response.json({ orders: result.rows });
},
};
Note: Hyperdrive requires your Postgres to be publicly accessible. Ensure you've configured SSL encryption and proper firewall rules.
Database Webhooks for Event-Driven Architecture
Rather than polling your database from external functions, push events to them using webhooks. Self-hosted Supabase supports this through the pg_net extension.
Setting Up Outbound Webhooks
-- Enable pg_net if not already
CREATE EXTENSION IF NOT EXISTS pg_net;
-- Create a function that calls your external service
CREATE OR REPLACE FUNCTION notify_order_created()
RETURNS TRIGGER AS $$
BEGIN
PERFORM net.http_post(
url := 'https://your-worker.workers.dev/webhook/order-created',
body := jsonb_build_object(
'event', 'order.created',
'order_id', NEW.id,
'user_id', NEW.user_id,
'total', NEW.total,
'timestamp', now()
),
headers := jsonb_build_object(
'Content-Type', 'application/json',
'X-Webhook-Secret', current_setting('app.webhook_secret')
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Attach to your orders table
CREATE TRIGGER on_order_created
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION notify_order_created();
Securing Webhooks
Always verify webhook signatures in your receiving function:
// Cloudflare Worker webhook handler
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const signature = request.headers.get('X-Webhook-Secret');
if (signature !== env.WEBHOOK_SECRET) {
return Response.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = await request.json();
// Process the webhook...
return Response.json({ received: true });
},
};
Performance Optimization
Connection Reuse
All three platforms support connection reuse, but implementation differs:
- Lambda: Create client outside handler, use connection pooling with
max: 1 - Vercel: Same pattern as Lambda; functions are Node.js under the hood
- Workers: Create client on first request; Workers persist across requests
Regional Proximity
Deploy your functions in the same region as your self-hosted Supabase instance. AWS Lambda and Vercel let you choose regions explicitly. Cloudflare Workers run globally, but Hyperdrive maintains pools in optimal locations.
Caching Strategies
For read-heavy workloads, add caching layers:
// Vercel with edge caching
export const runtime = 'edge';
export const revalidate = 60; // Cache for 60 seconds
// Cloudflare with KV caching
const cached = await env.KV.get(`users:${userId}`);
if (cached) return Response.json(JSON.parse(cached));
When to Use Each Platform
AWS Lambda: Best for complex backend logic, long-running processes, and tight AWS ecosystem integration. Choose Lambda when you need SQS queues, Step Functions, or run processes longer than 60 seconds.
Vercel Functions: Ideal when your frontend is already on Vercel. Zero configuration overhead, integrated with your deployment pipeline. Best for API routes that serve your application directly.
Cloudflare Workers: Optimal for latency-critical paths, global distribution, and lightweight transformations. Sub-5ms cold starts make them suitable for real-time applications.
Supascale and External Compute
While Supascale focuses on managing your self-hosted Supabase deployment—handling backups, custom domains, and OAuth configuration—your external compute platforms integrate seamlessly with any Supascale-managed instance. The same API endpoints and service role keys work whether you deployed manually or through Supascale's one-click setup.
For teams running production workloads, this combination offers the best of both worlds: managed Supabase infrastructure without cloud pricing, plus the flexibility to use any serverless platform for compute.
