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.
- Pre-rendering overview
- Rendering endpoints
- Complete Supabase examples
- Canonical regex sources — Crawler User-Agent, Static-asset extensions
Contents
- How it works
- When to use
- Quick start
- Framework examples
- Deploy and route the domain
- Validation
- Common issues
- Related
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:
- 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 byROOT_URL(visitor). Use this when the site's public domain is pointed at Supabase via a custom domain or a CDN path rewrite. - 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.ioor 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
-
Copy the chosen framework example from
examples/supabase/intosupabase/functions/<function-name>/index.ts. -
Copy
examples/supabase/shared.tsintosupabase/functions/_shared/ostr.ts(or alongside the function). -
Add your domain in the ostr.io pre-rendering panel; copy the
Basic ...token. -
Set secrets:
supabase secrets set OSTR_AUTH='Basic <base64 user:password>' supabase secrets set ROOT_URL='https://example.com' -
Deploy:
supabase functions deploy <function-name> --no-verify-jwt -
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.
| Framework | Example | Shape | Best for |
|---|---|---|---|
| Plain Deno | deno.ts | Reverse proxy | Smallest footprint, no app framework |
| Hono | hono.ts | Reverse proxy or inline | Lightweight router, ergonomic middleware |
| Oak | oak.ts | Reverse proxy or inline | Koa-style middleware chain |
| Fresh | fresh-middleware.ts | Inline in routes/_middleware.ts | Existing 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
renderTargetfromROOT_URL, never from the request host (preview URLs, the raw*.supabase.cohostname, or a custom domain still propagating will be rejected). 401/403from renderer — theAuthorizationheader was forwarded from the inbound request instead of replaced. The examples explicitlyheaders.delete('authorization')before settingOSTR_AUTH. Keep that order.- Function rejects with
401from Supabase — the function was deployed without--no-verify-jwtand 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.tsstill 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/HEADtraffic returning renderer output — the method guard was removed. The first gate in every example short-circuits non-GET/HEADrequests to origin.
Related
- Vercel integration — same decision tree, Vercel Routing Middleware runtime
- Cloudflare Worker integration — CDN-level alternative when Supabase is only the backend
- Next.js integration — Next.js
middleware.ts/ NPM package - Node.js NPM integration —
spiderable-middlewarefor Express / Koa / Fastify / vanillahttp - Rendering endpoints
- Detect pre-rendering engine requests at runtime