HTTP functions
Answer any method on any sub-path with your own status, headers and body: webhooks that verify signatures on the raw body, OAuth callbacks, GET APIs and files, with Web Request/Response and Deno.serve.
Every function answers /functions/v1/<name> and every path below it (/functions/v1/<name>/users/42), on every method. How the request reaches your code depends on the function's mode.
| Mode | Your handler gets | It returns |
|---|---|---|
| JSON (default) | The JSON body as the event; context.http has the method, sub-path, query and headers | A value, sent back as JSON with status 200 |
| HTTP | The whole request: method, sub-path, query, headers, raw body (bytes), client IP | A response: status, headers and any body (text, JSON, binary, a redirect) |
#Make a function an HTTP function
- Write a handler that is HTTP by itself: Python
def fetch(request), JavaScript/TypeScriptexport default { fetch(req, ctx) {} }orDeno.serve(handler). - Or keep
handlerand turn HTTP mode on: console › Functions › your function › Who can call it › "HTTP function",sov fn deploy --http, orPUT /api/v1/fn/functions/<name>/http {"http": true}.
export default {
async fetch(req: Request, ctx) { // ctx.path = the sub-path, ctx.caller = who called
const url = new URL(req.url)
if (req.method === 'GET' && ctx.path.startsWith('/users/')) {
return Response.json({ id: ctx.path.split('/')[2], q: url.searchParams.get('q') })
}
ctx.waitUntil(fetch('https://hooks.example.com/seen', { method: 'POST' })) // finishes before the call ends
return new Response('Not found', { status: 404 })
},
}Go functions in HTTP mode get the request as the event (method, path, query_string, headers, body_b64, ip) and return {"statusCode": 200, "headers": {…}, "body": "…", "isBase64Encoded": false}.
#Verify a webhook signature
Signatures are computed over the exact bytes the provider sent, so verify them on the raw body. Make the function public (console › Who can call it, or PUT …/access {"public": true}): providers call without a key. Keep the signing secret in the function's secrets.
import hashlib, hmac, os, time
from sovaria_functions import Response
def handler(request):
parts = dict(p.split("=", 1) for p in request.headers.get("stripe-signature", "").split(",") if "=" in p)
signed = f"{parts.get('t')}.".encode() + request.body
expected = hmac.new(os.environ["STRIPE_WEBHOOK_SECRET"].encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
return Response("bad signature", status=400)
event = request.json()
# … handle event["type"] …
return Response.json({"received": True})#Call it
const res = await sovaria.functions.fetch('api', 'users/42', { method: 'PUT', body: JSON.stringify(user),
headers: { 'Content-Type': 'application/json' } })
if (res.status === 201) console.log(await res.json())#What Sovaria adds and removes
- Your Sovaria credentials (
Authorization,apikey, Sovaria cookies) and the gateway's internal headers are never passed to your code. Who called is incontext.caller(user id, role). A keyless call to a public function keeps its ownAuthorizationheader (a provider's bearer token). - Your app's cookies reach the function only on your custom domain; on the shared
<ref>.sovaria.inhost the browser also sends cookies for the platform domain, which never reach customer code. - On the shared project host, HTML (and SVG/XML) responses are sent as
text/plainwith a sandbox CSP, as Supabase does: the console and API live on the same site. Serve pages from web hosting (a rewrite to the function) or a custom domain. - A response may not set framing headers (
Content-Length,Transfer-Encoding,Content-Encoding),Strict-Transport-SecurityorX-Sovaria-*; cookies are host-only (noDomain) and may not use Sovaria's cookie names. - The WAF, the function's policy (allowed origins, payload limit, rate limit), the plan's concurrency and the monthly quota apply to every call, whatever the method.
- The gateway waits for your plan's function timeout (
fn.timeout_sec, plus a small margin), so a 5-minute report on Ultra returns its result instead of a 504.
| Free | Pro | Ultra | Team | Enterprise | |
|---|---|---|---|---|---|
| Largest request body for an HTTP function call | 1 MB | 6 MB | 10 MB | 20 MB | 20 MB |
| Function timeouts | 10 s | 1 min | 5 min | 15 min | 15 min |
| Concurrent function invocations | 5 | 50 | 200 | 500 | 2,000 |
Responses are up to 10 MB. Bodies above the plan's fn.http_body_bytes get 413. A JSON function called with a body that isn't JSON gets 400 with a hint to switch to HTTP mode.
#Coming from Firebase or Supabase?
| Platform | What this is called there |
|---|---|
| Firebase | Cloud Functions HTTP functions (onRequest) |
| Supabase | Supabase Edge Functions Request/Response, Deno.serve |
| Sovaria | HTTP functions |
Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.