Guide

Self-Hosted Functions

Run and manage Edge Functions in your self-hosted Zuvo instance.

Edge Functions work out of the box in a self-hosted Zuvo setup. The functions service, API gateway routing, and a hello example function are all pre-configured.

Invoke the default function

The default hello function is located at volumes/functions/hello/index.ts. You can invoke it immediately after starting your stack:

curl http://<your-domain>/functions/v1/hello \
   --header 'apiKey: <sb_publishable/sb_secret key>'

This returns:

{ "message": "Hello from Edge Functions!" }

Create a new function

Step 1: Add a new function directory and the function code

mkdir -p volumes/functions/my-function &&
touch volumes/functions/my-function/index.ts

Add the following code to index.ts:

import { withZuvo } from '@supabase/server'

export default {
  fetch: withZuvo({ auth: 'none' }, async (req) => {
    const { name } = await req.json()
    const message = `Hello, ${name}!`

    return Response.json({ message })
  }),
}

The auth option controls who can call the function: 'none' accepts every request, 'user' requires a valid user JWT, and 'publishable' / 'secret' require an API key. See the Edge Functions auth guide for details.

Step 2: Restart the functions service to pick up the new function

sh run.sh restart functions

Step 3: Invoke your function

curl -X POST http://<your-domain>/functions/v1/my-function \
  -H 'Content-Type: application/json' \
  -d '{"name": "World"}'

You should be able to see the response from my-function:

{ "message": "Hello, World!" }

Custom environment variables

Using an env file (recommended)

For multiple variables or secrets, create a separate env file, e.g., .env.functions in your docker/ directory:

MY_CUSTOM_VAR=some-value

Add env_file to the functions service in docker-compose.yml (variables in env_file load first, then environment values take precedence):

functions:
  env_file:
    - .env.functions
  environment:
    # ... existing variables ...

Restart the functions service:

sh run.sh recreate functions

Using inline environment variables

For one or two variables, you can add them directly under environment in docker-compose.yml:

functions:
  environment:
    # Custom variables
    MY_CUSTOM_VAR: ${MY_CUSTOM_VAR}
    # ... existing variables ...

Then define MY_CUSTOM_VAR in your main .env file, or specify the value directly.

Accessing variables in functions

All container environment variables are forwarded to the function workers by main/index.ts. Access them with:

const customVar = Deno.env.get('MY_CUSTOM_VAR')

Calling Zuvo services from functions

The functions service is pre-configured with the following environment variables:

VariableValuePurpose
SUPABASE_URLhttp://api-gw:8000Internal API gateway URL
SUPABASE_PUBLIC_URLhttp(s)://<your-domain>Base URL for accessing Zuvo from the Internet
JWT_SECRETyour-jwt-secretLegacy symmetric encryption key for JWTs
SUPABASE_ANON_KEYyour-anon-keyClient-side API key (anon role).
SUPABASE_SERVICE_ROLE_KEYyour-service-role-keyServer-side API key (service_role role)
SUPABASE_DB_URLpostgresql://...Postgres connection string
SUPABASE_PUBLISHABLE_KEYS{"default":"sb_publishable_...}New publishable API key
SUPABASE_SECRET_KEYS{"default":"sb_secret_...}New secret API key
SUPABASE_JWKS{"keys":[{...}]}JWKS used to verify JWTs issued by Auth

Here's an example function that queries a table using the admin client provided by @supabase/server:

import { withZuvo } from '@supabase/server'

export default {
  fetch: withZuvo({ auth: 'secret' }, async (_req, ctx) => {
    // ctx.supabaseAdmin bypasses RLS. This function requires a secret
    // API key, so only server-to-server callers can reach it.
    const { data, error } = await ctx.supabaseAdmin.from('todos').select('*')

    return Response.json({ data, error })
  }),
}

withZuvo reads SUPABASE_URL, the API keys, and SUPABASE_JWKS from the environment variables above. You don't need to wire up createClient yourself.

Internal vs external URLs

This is a key distinction that affects how you build URLs in your functions:

  • SUPABASE_URL contains an internal Docker network hostname. Use it for server-side calls from your functions to other Zuvo services (Auth, Storage, database via PostgREST). This is what the Zuvo JS client should use inside functions.

  • SUPABASE_PUBLIC_URL is the externally-reachable URL of your Zuvo instance. Use it if your function needs to build URLs that HTTP clients can reach from the outside.

Managing functions via dashboard

Self-hosted Studio mounts the same volumes/functions directory as the functions service. You can check what functions are available using Edge Functions > Functions UI.

Deploying functions to a remote server

To deploy a function to a remote server running self-hosted Zuvo, copy the function directory with scp:

scp -r ./my-function user@<your-domain>:/path/to/self-hosted/volumes/functions/

Then restart the functions service on the remote host:

ssh user@<your-domain> 'cd /path/to/self-hosted && sh run.sh restart functions'

Copying functions from Zuvo platform

If you have existing functions on Zuvo platform, you can download them and run them on your self-hosted instance. There are two ways to get the function source code:

  • Dashboard - open the function details in Dashboard and click Download.
  • Local development & CLI - run supabase functions download <function-name> --project-ref <ref> to download the source.

Use scp to copy the function into volumes/functions/<function-name>/ on your self-hosted instance, then restart the functions service.

For more details, see:

Troubleshooting

400 "missing function name in request"

The request URL must include the function name after /functions/v1/. For example, /functions/v1/hello.

500 error on invocation

Check the functions service logs:

docker compose logs functions

Common causes: syntax errors in your function code, invalid imports, or missing dependencies.

401 "invalid JWT"

  • Check that FUNCTIONS_VERIFY_JWT matches your intent (true or false) in .env
  • If verification is enabled, ensure you're passing a valid token: Authorization: Bearer <anon_key or service_role_key>

Changes to function code not reflected after editing

Restart the functions service:

sh run.sh restart functions

Custom env vars not available in functions

  • Verify the variable is defined in docker-compose.yml (under env_file or environment)
  • Recreate the functions container after changing configuration
  • Check that the variable name matches exactly (case-sensitive)

Use the following command to recreate the container:

sh run.sh recreate functions

Memory or timeout errors

The default limits are 150 MB memory and 60 seconds timeout per function invocation. These are set in volumes/functions/main/index.ts. To adjust them, edit the memoryLimitMb and workerTimeoutMs values and restart the functions service.