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.
On managed Supabase platform, Edge Functions are deployed across multiple regions. Self-hosted standalone instance configuration resembles a standard serverless setup.
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.tsAdd 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 functionsStep 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#
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-valueAdd 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 ...Don't commit .env.functions to version control if it contains secrets. Add it to your .gitignore.
Restart the functions service:
sh run.sh recreate functionsUsing 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 = ..('MY_CUSTOM_VAR')Calling Supabase services from functions#
The functions service is pre-configured with the following environment variables:
| Variable | Value | Purpose |
|---|---|---|
SUPABASE_URL | http://api-gw:8000 | Internal API gateway URL |
SUPABASE_PUBLIC_URL | http(s)://<your-domain> | Base URL for accessing Supabase from the Internet |
JWT_SECRET | your-jwt-secret | Legacy symmetric encryption key for JWTs |
SUPABASE_ANON_KEY | your-anon-key | Client-side API key (anon role). |
SUPABASE_SERVICE_ROLE_KEY | your-service-role-key | Server-side API key (service_role role) |
SUPABASE_DB_URL | postgresql://... | 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.
auth: 'user' verifies caller JWTs against SUPABASE_JWKS. If you're on a legacy setup without it configured, see New API Keys and Asymmetric Authentication.
Internal vs external URLs#
This is a key distinction that affects how you build URLs in your functions:
-
SUPABASE_URLcontains 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_URLis 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> --remote=<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:
Memory management#
The functions service has two memory limits: a shared container budget and a per-worker limit. Idle-worker cleanup responds to usage of the container budget.
Container memory budget#
FUNCTIONS_MEMORY_LIMIT defaults to 2g, or 2 GiB, in the main docker/.env file. All user workers, the main worker, and page cache share this budget. The monitor excludes inactive file cache when deciding whether to clean up idle workers.
Docker Compose applies this setting through mem_limit. The monitor reads the applied cgroup limit. This is a service resource setting, separate from custom environment variables that function code reads through Deno.env.get().
To change the container budget:
-
In the main
docker/.envfile, set the limit for your host and functions' memory requirements:FUNCTIONS_MEMORY_LIMIT=2g -
From the
docker/directory, recreate the functions container:sh run.sh recreate functions
A restart alone does not apply a changed container limit.
Choose a budget that leaves memory available for the other services on your host. The limit does not reserve host RAM. A 2 GiB budget does not guarantee capacity for 8 workers using 256 MiB each, because workers share the container with other memory consumers.
Per-worker memory limit#
memoryLimitMb in volumes/functions/main/index.ts defaults to 256 MiB per worker. A worker can reach its own limit while the container still has memory available. Increasing this limit allows each worker to consume more of the shared container budget.
To change the per-worker limit:
-
Edit
memoryLimitMbinvolumes/functions/main/index.ts. -
From the
docker/directory, restart the functions service:sh run.sh restart functions
Idle-worker cleanup#
The monitor uses the container's working set: memory.current minus inactive_file from memory.stat, with a minimum of zero.
The main worker samples the working set every second while it is below 80% of the container limit. At or above 80%, it asks eligible idle workers to retire and requests allocator memory collection. Cleanup skips workers with active requests or pending tasks, including tasks registered with EdgeRuntime.waitUntil().
The monitor waits 5 seconds after each cleanup attempt before sampling again, even when no workers acknowledge retirement. It logs a cleanup warning only when at least one worker acknowledges retirement. An acknowledgment does not mean worker shutdown or memory reclamation has finished. Retiring workers can cause cold starts on later requests.
Monitoring requires a finite cgroup v2 limit and readable memory.current, memory.max, and memory.stat files under /sys/fs/cgroup. An unlimited limit or unavailable files disable monitoring with a warning. After fixing the configuration, run sh run.sh recreate functions from docker/. Other failures trigger retries every 5 seconds, with one warning until recovery.
Per-worker limits still apply. The monitor does not use host memory measurements or guarantee protection from out-of-memory errors. Active tasks, stricter parent cgroup limits, or host memory shortages can still interrupt requests.
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 functionsFor 503 BOOT_ERROR responses without (ref: <uuid>) in the message, check for syntax errors, invalid imports, or missing dependencies that prevent the function from starting.
If the error message includes (ref: <uuid>), the failure occurred in the main worker. Run docker compose logs functions and search for that reference to find the corresponding error.
401 "invalid JWT"#
For more on 401 responses from functions, see Edge Function 401 errors.
- Check that
FUNCTIONS_VERIFY_JWTmatches your intent (trueorfalse) 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 ansb_publishable_*orsb_secret_*key in theapikeyheader. The API gateway translatessb_keys to an internal JWT, which the functions service then verifies. - Verifying asymmetric JWTs, including translated
sb_keys, requiresSUPABASE_JWKSto be set for thefunctionsservice indocker-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 functionsCustom env vars not available in functions#
- Verify the variable is defined in
docker-compose.yml(underenv_fileorenvironment) - 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 functionsTimeout errors#
The default timeouts in volumes/functions/main/index.ts apply to each worker:
| Setting | Default | Description |
|---|---|---|
workerTimeoutMs | 400,000 ms | Maximum worker lifetime. |
requestAbsentTimeoutMs | 60,000 ms | Allows 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.