Skip to content

Backend

Supabase Edge Functions — Where Velocity X Runs Its OAuth Refreshes, Cron Jobs, and Service-Role Operations

Deno runtime, service-role JWT, scheduled invocations, and why Edge Functions beat Netlify Functions for DB-adjacent work

🔄 🗄️

If you're building SaaS on Supabase, there's a moment—usually 3am on a Sunday—when you realize: "I need this job to run every hour. I need it to touch the database with elevated permissions. And I need it to be fast because it's blocking user requests." Enter Supabase Edge Functions. They're Deno-powered serverless that run in Supabase's own infrastructure, co-located with your database, zero cold-start latency for OAuth refreshes and scheduled work. Velocity X uses them for token refresh, hourly cron syncs, and cross-tenant SLT rollups.

Edge Functions vs Netlify Functions vs Database Triggers

You have three choices for background work on Supabase: (1) Database triggers (Postgres-native), (2) Netlify Functions (Node.js, billed separately), or (3) Supabase Edge Functions (Deno, same instance). Triggers are fast and transactional but can't call external APIs. Netlify Functions are familiar Node but incur cold starts and egress costs. Edge Functions: instant startup, same VPC as your database, service-role JWT in the runtime, scheduled invocations built-in. For anything that reads/writes the database AND calls an external API (like OAuth token refresh), Edge Functions are the play.

Architecture: Service Role JWT + Scheduled Invocations

Every Edge Function gets the Supabase URL and service-role key injected at runtime (via Netlify env vars). Unlike user-scoped RLS, service role bypasses all policies—use it to bill all orgs, sync data across tenants, or trigger long-running webhooks. Schedule it via the Supabase CLI: --schedule "0 * * * *" for hourly. Velocity X schedules data syncs every hour and OAuth refreshes on login. The function wakes, hits the database, and sleeps. Costs: $1 per 1M invocations. You'll never hit that.

Three Production Examples

1. OAuth Token Refresh (Google + Meta). Users log in via Google; your Edge Function stores the refresh token server-side. Every 30 days, the function wakes, decrypts the token, calls Google's token endpoint, and stores the new one. If refresh fails, it marks the user account as "token-stale" for the frontend to re-prompt. No user request hangs waiting for the API call:

{`export default async (req: Request) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  const { data: users } = await supabase
    .from('users')
    .select('id, oauth_provider, encrypted_refresh_token')
    .eq('token_expires_at', '<', new Date().toISOString());

  for (const user of users) {
    const refreshToken = await decrypt(user.encrypted_refresh_token);
    const newToken = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      body: new URLSearchParams({
        client_id: Deno.env.get('GOOGLE_CLIENT_ID')!,
        client_secret: Deno.env.get('GOOGLE_CLIENT_SECRET')!,
        refresh_token: refreshToken,
        grant_type: 'refresh_token'
      })
    }).then(r => r.json());

    await supabase
      .from('users')
      .update({
        encrypted_access_token: await encrypt(newToken.access_token),
        token_expires_at: new Date(Date.now() + newToken.expires_in * 1000)
      })
      .eq('id', user.id);
  }

  return new Response(JSON.stringify({ refreshed: users.length }));
};`}

2. Hourly Cron: Data Sync Across Tenants. Velocity X syncs location pins, hail-claim status, and team member assignments hourly. The function queries a webhook-events queue (stored in Supabase), processes each event, and updates the canonical tables. RLS would block cross-tenant reads; service role doesn't. Sync completes in 2–5 seconds; scheduled to run at :15 past each hour so it doesn't collide with user-peak traffic:

{`export default async (req: Request) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  const { data: events } = await supabase
    .from('webhook_queue')
    .select('*')
    .eq('processed', false)
    .limit(100);

  for (const event of events) {
    await processEvent(event, supabase);
    await supabase
      .from('webhook_queue')
      .update({ processed: true, processed_at: new Date() })
      .eq('id', event.id);
  }

  return new Response(JSON.stringify({ processed: events.length }));
};`}

3. Webhook Receiver: Stripe Subscription Changes. Stripe POSTs to your Edge Function endpoint. The function updates subscription status, logs the event, and—because it uses service role—writes to any org's billing table without RLS friction. No user JWT needed; service role handles the auth. Store the webhook signing secret in env and validate before processing:

{`export default async (req: Request) => {
  const signature = req.headers.get('stripe-signature')!;
  const body = await req.text();
  const event = JSON.parse(body);

  // Validate signature
  const expectedSig = crypto.subtle.sign(
    'HMAC',
    await crypto.subtle.importKey('raw', Deno.env.get('STRIPE_WEBHOOK_SECRET')!, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']),
    new TextEncoder().encode(body)
  );

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  );

  if (event.type === 'customer.subscription.updated') {
    await supabase
      .from('subscriptions')
      .update({ status: event.data.object.status })
      .eq('stripe_id', event.data.object.id);
  }

  return new Response(JSON.stringify({ ok: true }));
};`}

Common Questions

Can I call external APIs from Edge Functions?

Yes. They run in Supabase's datacenter, so latency to third-party APIs is the same as from anywhere. Deno's native fetch works without extra libraries.

What's the difference between Deno and Node?

Deno is TypeScript-first, ships with no node_modules, and has stricter permissions by default. For Supabase, it means zero cold-start and no package.json bloat. Node has more libraries; Deno is faster to deploy.

How do I schedule an Edge Function?

Via the CLI: supabase functions deploy my_function --schedule "0 * * * *". Or use pg_cron inside a database trigger for sub-minute granularity. Edge Functions are better for "run this every N hours"; pg_cron is better for "run this 3 times per second".

Is service role safe?

Only if you control the code. Edge Functions are your code; use service role freely. Never expose the key to the frontend. If a key leaks, rotate it immediately in Supabase's dashboard.

Can I test Edge Functions locally?

Yes. supabase functions serve spins up a local Deno runtime. Env vars come from .env.local. Same code runs in production.

What if my function times out?

Scheduled Edge Functions have a 5-minute timeout. Long-running syncs should batch work: process 100 events, return, let the cron trigger the next batch 5 minutes later. This also keeps logs readable.

The Bottom Line

Supabase Edge Functions are the third leg of the stool: RLS + JWT claims secure the rows, Edge Functions move data and refresh tokens at scale without user-request latency, and the database stays the single source of truth. Build with service role, test locally, schedule it, ship it. In production, you'll log in and never think about that token refresh. That's the goal.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.