Supabase Integration

April 20, 2026 · View on GitHub

ostr.io pre-rendering SEO middleware for apps whose HTML is served from Supabase Edge Functions — Deno-native, standard Fetch API. Each example is a drop-in middleware that detects crawler traffic and proxies matching page requests to the ostr.io renderer. Application code is untouched; visitor traffic remains on origin.

Contents

How it works

Supabase Edge Functions run on a Deno-compatible isolate at https://<project>.supabase.co/functions/v1/<name> and accept any standard fetch-style handler. Two integration shapes fit the pre-rendering decision tree:

  1. Reverse-proxy Edge Function — a dedicated function acts as the public entry point for the site. It runs the UA / method / static-asset check, then either proxies to https://render.ostr.io (crawler) or to the real origin defined by ROOT_URL (visitor). Use this when the site's public domain is pointed at Supabase via a custom domain or a CDN path rewrite.
  2. Inline middleware in an existing SSR function — the same decision tree runs inside a Deno-native SSR framework (Hono, Oak, Fresh). Crawler requests short-circuit to the renderer; visitor requests fall through to the app's normal routes. Use this when HTML is already generated by an Edge Function.

Both shapes share the same canonical behavior — crawler User-Agent matcher, static-asset extension matcher, legacy _escaped_fragment_ handling, and fail-open fallback on renderer errors — and are exported from examples/supabase/shared.ts so every framework variant stays in sync.

When to use

  • HTML is served from a Supabase Edge Function — Deno-native SSR via Hono, Oak, Fresh, or plain Deno.serve.
  • You want a reverse-proxy Edge Function in front of a separate origin (Vercel, Netlify, a container, etc.) and prefer to keep routing concerns on Supabase.
  • The site is small enough that per-request Edge Function invocation cost is acceptable. Edge Functions have no built-in HTML cache tier; pair with render-cache.ostr.io or a CDN in front for high-traffic sites.

Do not use this integration when Supabase is only the backend (Postgres, Auth, Storage) behind a frontend hosted on Vercel, Netlify, Cloudflare, or a reverse-proxy origin. The middleware belongs at the actual HTML layer — see Vercel, Netlify, Cloudflare Worker, Nginx, Apache, or Caddy.

Quick start

  1. Copy the chosen framework example from examples/supabase/ into supabase/functions/<function-name>/index.ts.

  2. Copy examples/supabase/shared.ts into supabase/functions/_shared/ostr.ts (or alongside the function).

  3. Add your domain in the ostr.io pre-rendering panel; copy the Basic ... token.

  4. Set secrets:

    supabase secrets set OSTR_AUTH='Basic <base64 user:password>'
    supabase secrets set ROOT_URL='https://example.com'
    
  5. Deploy:

    supabase functions deploy <function-name> --no-verify-jwt
    
  6. Route the site domain through the function (see Deploy and route the domain).

Framework examples

Pick the closest example, then adjust imports and route matchers.

FrameworkExampleShapeBest for
Plain Denodeno.tsReverse proxySmallest footprint, no app framework
Honohono.tsReverse proxy or inlineLightweight router, ergonomic middleware
Oakoak.tsReverse proxy or inlineKoa-style middleware chain
Freshfresh-middleware.tsInline in routes/_middleware.tsExisting Fresh SSR app

All four share shared.ts for the UA / static / render-fetch decision — update once, apply everywhere.

Deploy and route the domain

The Edge Function's default public URL is https://<project-ref>.supabase.co/functions/v1/<function-name>. To make it serve https://example.com/, pick one:

  • Supabase custom domain — configure a custom domain on the project; requests to example.com/functions/v1/<function-name>/* reach the function. Pair with a URL rewrite in your DNS/edge provider to drop the /functions/v1/<function-name> prefix so visitors see clean URLs.
  • Cloudflare (or similar) in front — keep DNS on Cloudflare (orange-cloud proxy), add a Worker or Transform Rule that rewrites example.com/* to <project-ref>.supabase.co/functions/v1/<function-name>/*. If you are already fronting the site with Cloudflare, consider the Cloudflare Worker integration instead — same edge, no Supabase hop.
  • Deploy-time wiring — when using Fresh or any SSR framework that serves the full app from a single Edge Function, the inline middleware variant sits inside the existing entry point; no extra routing step is required.

Functions that need to accept unauthenticated public traffic must be deployed with --no-verify-jwt. See the Supabase function auth docs.

Validation

Bot request should return a pre-rendered snapshot with X-Prerender-Id:

curl -sI -A 'Googlebot/2.1' https://example.com/some-page

Regular browser request should pass through to the origin (or the app):

curl -sI -A 'Mozilla/5.0' https://example.com/some-page

Legacy fragment request should hit the renderer:

curl -sI 'https://example.com/some-page?_escaped_fragment_='

Inside the Supabase Studio Edge Functions → Logs panel, bot requests should show a single outbound fetch to render.ostr.io; visitor requests should show a single outbound fetch to ROOT_URL.

Common issues

  • Domain mismatch from renderer — the ostr.io renderer only accepts registered domains. Always build renderTarget from ROOT_URL, never from the request host (preview URLs, the raw *.supabase.co hostname, or a custom domain still propagating will be rejected).
  • 401 / 403 from renderer — the Authorization header was forwarded from the inbound request instead of replaced. The examples explicitly headers.delete('authorization') before setting OSTR_AUTH. Keep that order.
  • Function rejects with 401 from Supabase — the function was deployed without --no-verify-jwt and public traffic cannot invoke it. Redeploy.
  • Every request hits the function (including images) — the static-asset check was skipped or the matcher is wrong. Verify shared.ts still uses the canonical static-extension regex.
  • Cold-start tail latency — Edge Functions cold-start on low traffic. Pre-render on a cache-friendly endpoint (render-cache.ostr.io) or front the function with a CDN cache for bot paths.
  • Non-GET/HEAD traffic returning renderer output — the method guard was removed. The first gate in every example short-circuits non-GET/HEAD requests to origin.