DEV Community

Vigilmon
Vigilmon

Posted on

Uptime Monitoring for Supabase Applications (Free, Multi-Region)

Uptime Monitoring for Supabase Applications (Free, Multi-Region)

Supabase is a full backend-as-a-service: Postgres, auth, real-time subscriptions, storage, and edge functions bundled together. Developers love it because you ship fast. But "shipping fast" also means it's easy to skip the step where you find out your edge function is down before your users do.

This guide covers the three failure modes Supabase developers hit most often, then walks you through setting up external uptime monitoring — free tier, no credit card.


The three failure modes in Supabase apps

Edge function cold starts + timeouts — Supabase edge functions run on Deno Deploy. Cold starts are fast, but if your function exceeds the 150 ms CPU budget or the 2-second wall-clock timeout under load, it starts returning 546 errors. You won't see these in your Supabase dashboard unless you're watching logs in real time.

Row Level Security misconfigurations — A bad RLS migration deploys, your tables go unreadable to authenticated users, and every client-side query returns an empty array. The app doesn't throw — it just silently shows nothing. Health checks that only ping a public endpoint will miss this entirely.

Custom backend downtime — Most Supabase apps also run a Node.js, Python, or Go backend that talks to Supabase via the service key. That backend can go down independently of Supabase itself. Your Supabase dashboard will show all green while your users are hitting a dead API.


Step 1: Add a health endpoint to your backend

If you're running a Node.js backend (Express, Fastify, etc.) alongside Supabase, add a /health route that checks the Supabase connection:

// src/routes/health.ts
import { Request, Response } from 'express'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
)

export async function healthCheck(req: Request, res: Response) {
  const checks: Record<string, { status: string; latencyMs?: number; error?: string }> = {}

  // Check Supabase connectivity
  const dbStart = Date.now()
  try {
    const { error } = await supabase.from('_health_probe').select('1').limit(1).single()
    // If the table doesn't exist, we get a "not found" error — that's fine, the DB is reachable
    if (error && error.code !== 'PGRST116') {
      checks.supabase = { status: 'error', error: error.message }
    } else {
      checks.supabase = { status: 'ok', latencyMs: Date.now() - dbStart }
    }
  } catch (err: any) {
    checks.supabase = { status: 'error', error: err.message }
  }

  // Check Supabase Auth service
  const authStart = Date.now()
  try {
    const { error } = await supabase.auth.getSession()
    checks.auth = { status: 'ok', latencyMs: Date.now() - authStart }
  } catch (err: any) {
    checks.auth = { status: 'error', error: err.message }
  }

  const allOk = Object.values(checks).every((c) => c.status === 'ok')

  res.status(allOk ? 200 : 503).json({
    status: allOk ? 'ok' : 'degraded',
    checks,
    timestamp: new Date().toISOString(),
  })
}
Enter fullscreen mode Exit fullscreen mode

Register the route:

// src/app.ts
import { healthCheck } from './routes/health'
app.get('/health', healthCheck)
Enter fullscreen mode Exit fullscreen mode

Alternatively, if you're using Supabase edge functions, create a dedicated health function:

// supabase/functions/health/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (_req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? ''
  )

  try {
    const start = Date.now()
    const { error } = await supabase.from('_health_probe').select('count').limit(1)
    const latency = Date.now() - start

    if (error && error.code !== 'PGRST116') {
      return new Response(
        JSON.stringify({ status: 'error', error: error.message }),
        { status: 503, headers: { 'Content-Type': 'application/json' } }
      )
    }

    return new Response(
      JSON.stringify({ status: 'ok', latencyMs: latency }),
      { status: 200, headers: { 'Content-Type': 'application/json' } }
    )
  } catch (err) {
    return new Response(
      JSON.stringify({ status: 'error', error: err.message }),
      { status: 503, headers: { 'Content-Type': 'application/json' } }
    )
  }
})
Enter fullscreen mode Exit fullscreen mode

Deploy it:

supabase functions deploy health
Enter fullscreen mode Exit fullscreen mode

Step 2: Set up RLS health monitoring

RLS failures are sneaky. Create a dedicated health probe table that your monitoring can query:

-- Run in the Supabase SQL Editor
CREATE TABLE IF NOT EXISTS _health_probe (
  id serial PRIMARY KEY,
  checked_at timestamptz DEFAULT now()
);

-- Allow anon reads so the monitoring service can reach it
ALTER TABLE _health_probe ENABLE ROW LEVEL SECURITY;
CREATE POLICY "health probe readable by anon"
  ON _health_probe FOR SELECT TO anon USING (true);

-- Insert a static row
INSERT INTO _health_probe (id) VALUES (1) ON CONFLICT DO NOTHING;
Enter fullscreen mode Exit fullscreen mode

Now your health check can use the anon key (safe to expose) to verify that RLS is letting reads through.


Step 3: Configure external uptime monitoring

The health endpoint is useless if you only check it when something feels wrong. Set up an external monitor that checks it every minute from multiple regions and pages you if it fails.

  1. Go to vigilmon.online and sign up (free tier, no credit card).
  2. Create a new HTTP(S) monitor.
  3. Set URL to your health endpoint: https://your-app.com/health or https://<project-ref>.supabase.co/functions/v1/health.
  4. Set interval to 60s.
  5. Set expected status to 200.
  6. Under Regions, select at least two (e.g. EU West and US East).
  7. Add an alert channel — email, Slack, or webhook.

Save the monitor. Within a minute you'll see the first probe results.


Step 4: Monitor Supabase storage (optional)

If you use Supabase Storage heavily, add a storage check:

// In your health check function
const storageStart = Date.now()
try {
  const { data, error } = await supabase.storage.getBucket('your-bucket-name')
  if (error) {
    checks.storage = { status: 'error', error: error.message }
  } else {
    checks.storage = { status: 'ok', latencyMs: Date.now() - storageStart }
  }
} catch (err: any) {
  checks.storage = { status: 'error', error: err.message }
}
Enter fullscreen mode Exit fullscreen mode

What good looks like

After setup, your monitoring dashboard shows:

Check Status Latency
Supabase DB ✅ ok 23ms
Supabase Auth ✅ ok 18ms
Storage ✅ ok 31ms

And if any of those flip to error, your phone buzzes before any user notices.


Recap

  1. Add a /health endpoint (or edge function) that queries Supabase and returns structured JSON.
  2. Create a _health_probe table with an anon-readable RLS policy so your probe can detect auth/RLS regressions.
  3. Point a free multi-region monitor at the endpoint — vigilmon.online works with no credit card.
  4. Get alerted the moment any check degrades.

Supabase handles the infrastructure; you handle knowing when your layer of it breaks. Thirty minutes of setup now saves hours of frantic debugging later.

Top comments (0)