Skip to content
Self-Hosting

Self-Hosted Functions

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

Edge Functions work out of the box in a self-hosted Supabase 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 { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ auth: ['publishable', 'secret'] }, async (req) => {
const { name } = await req.json()
const message = `Hello, ${name}!`
return Response.json({ message })
}),
}

The auth option controls who can call the function. This example matches the default hello function and requires a publishable or secret API key in the apikey header. 'none' accepts every request, and 'user' requires a valid user JWT. 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 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
-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#

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):

docker-compose.yml
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:

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 = ..('MY_CUSTOM_VAR')

Calling Supabase 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 Supabase 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 { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ 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 })
}),
}

withSupabase 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 Supabase services (Auth, Storage, database via PostgREST). This is what the Supabase JS client should use inside functions.

  • SUPABASE_PUBLIC_URL is the externally-reachable URL of your Supabase 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 Supabase, 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 Supabase platform#

If you have existing functions on Supabase 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#

404 NOT_FOUND#

The request URL must include the function name after /functions/v1/. For example, /functions/v1/hello. Check that the corresponding directory exists at volumes/functions/hello/.

Function invocation errors#

Inspect the sb-error-code response header and see Edge Functions error codes for details about the error.

Check the functions service logs:

docker compose logs functions

For 503 BOOT_ERROR, check for syntax errors, invalid imports, or missing dependencies that prevent the function from starting.

401 "invalid JWT"#

  • Check that FUNCTIONS_VERIFY_JWT matches your intent (true or false) in .env
  • If verification is enabled, pass either a valid JWT in Authorization: Bearer <token> (a user session token or a legacy API key), or an sb_publishable_* or sb_secret_* key in the apikey header. The API gateway translates sb_ keys to an internal JWT, which the functions service then verifies.
  • Verifying asymmetric JWTs, including translated sb_ keys, requires SUPABASE_JWKS to be set for the functions service in docker-compose.yml. See New API Keys and Asymmetric Authentication.

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 in volumes/functions/main/index.ts apply to each worker:

SettingDefaultDescription
memoryLimitMb150 MBMaximum memory usage.
workerTimeoutMs400,000 msMaximum worker lifetime.
requestAbsentTimeoutMs60,000 msAllows the worker to shut down after 60 seconds without requests when no tasks are pending.

To adjust these limits, edit the values in volumes/functions/main/index.ts and restart the functions service.

Requests have a separate idle timeout of 150 seconds. This is set by --user-worker-request-idle-timeout in the functions service command in docker-compose.yml, with the value 150000 in milliseconds. Recreate the functions container after changing this value.

The Envoy gateway has separate limits of 160 seconds of inactivity and a 410-second upstream response timeout.

If you use the optional Kong gateway, read_timeout in volumes/api/kong.yml is set to 160000 milliseconds, allowing 160 seconds of inactivity between reads from the functions service.