Run your backend on AWS Lambda, step by step
Run your backend on AWS Lambda as one container image. Config and functions load from S3, Postgres can live anywhere, and your frontend keeps supabase-js.
You can run your backend on AWS Lambda by deploying instancez as a container function. The image runs inz serve behind the AWS Lambda Web Adapter, reads its config and function code from S3, and connects to any Postgres the function can reach. Your frontend code stays on @supabase/supabase-js.
We built instancez so the whole backend fits in one file and the binary runs anywhere, serverless included. On Lambda there’s no server to patch and nothing running while nobody calls your API. This post goes from the YAML to the function settings.
How does instancez run on Lambda?
instancez is one Go binary. On Lambda it ships as a container image built from Dockerfile.lambda. That image has three things in it:
inz serve, the same server you’d run on a VM- Node.js, for code functions
- the AWS Lambda Web Adapter
The adapter turns each Lambda invocation into a plain HTTP request to inz serve on port 8080. There’s no handler shim and no Lambda-specific code path in your backend. The REST API, auth, storage and functions behave the same way they do in Docker or on a VPS.
The default command is inz serve --migrate. Every cold start diffs your instancez.yaml against the database and applies the change. Migrations take a Postgres advisory lock with a lock timeout (default 5s). If the lock times out, the server keeps the last applied config.
What do you need before you start?
| Piece | What to use |
|---|---|
| Database | Any Postgres the function can reach over the network |
| Config and functions | An S3 object: the bare instancez.yaml, or a bundle from inz bundle |
| File storage | An S3 bucket (the S3 storage provider, not local disk) |
| Image | A private ECR repository with a single-arch image |
| Function | Package type Image, arm64, 512 MB or more |
The working directory on Lambda is read-only, which is why config comes from S3 and uploads go to S3. We checked that locally: with the scaffold’s default type: local storage, the server stops at startup with mkdir ./uploads: read-only file system.
Step 1: point storage at S3
Start from a normal project (inz init) and swap the storage provider:
providers:
storage:
type: s3
bucket: ${INSTANCEZ_ENV_S3_BUCKET}
region: ${INSTANCEZ_ENV_S3_REGION}
instancez fills in ${...} references when it loads the config. Config files may only reference variables in the INSTANCEZ_ENV_ namespace, and inz validate rejects anything else. Leave out access_key_id and secret_access_key and the S3 client uses the function’s IAM role.
INSTANCEZ_ENV_S3_BUCKET=my-app-files INSTANCEZ_ENV_S3_REGION=us-east-1 inz validate
✓ Schema valid
Everything else in the file stays the same: tables, RLS, auth, buckets, SQL RPC and functions. If the one-file model is new to you, one YAML file backend explains it.
Step 2: build the bundle and upload it to S3
If your project has no code functions, upload instancez.yaml to S3 and set INSTANCEZ_CONFIG=s3://my-bucket/my-app/instancez.yaml on the function.
If it has a functions: block, a bare YAML file isn’t enough, because it doesn’t carry the function source. Build a bundle instead. It packs instancez.yaml and functions/ with vendored npm dependencies into one archive:
cd functions && npm install && cd .. # once, so package-lock.json exists
inz bundle --output s3://my-bucket/my-app/bundle.tar.gz
✓ Schema valid
✓ Bundle uploaded: s3://my-bucket/my-app/bundle.tar.gz#1cbc56cb...
inz bundle runs npm ci, so it needs a committed package-lock.json. It also checks that every ${INSTANCEZ_ENV_...} the config references is set, but writes the reference into the bundle, not the value. Secrets stay in the function’s environment.
Before anything goes to Lambda, run the exact artifact yourself. Write it to a file and serve it against a local Postgres:
inz bundle --output bundle.tar.gz
inz serve --migrate --bundle ./bundle.tar.gz
That’s the same code path Lambda runs, minus the adapter. The CLI also prints a functions_bundle: line you could paste into the YAML. On Lambda we set INSTANCEZ_BUNDLE to the S3 URI instead, which is the route we tested.
Step 3: build the image and push it to ECR
This step comes from our Lambda guide. Lambda needs a single-arch image in a private ECR repository. The public ghcr.io/instancez/instancez:<version>-lambda tag is a multi-arch manifest list, and Lambda rejects manifest lists. So don’t pull, tag and push that image. Build the per-arch image from source:
VERSION=v0.0.11 # replace with the release/commit you want to pin
# Authenticate to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789012.dkr.ecr.us-east-1.amazonaws.com
# Build a single-arch (arm64) image from source and push it straight to ECR
git clone --branch ${VERSION} --depth 1 https://github.com/instancez/instancez.git
cd instancez
docker buildx build \
--platform linux/arm64 \
--provenance=false \
-f Dockerfile.lambda \
-t 123456789012.dkr.ecr.us-east-1.amazonaws.com/instancez/prod:${VERSION}-lambda-arm64 \
--push .
Step 4: configure the function
Create the function from that image with these settings:
| Setting | Value |
|---|---|
| Architecture | arm64 |
| Memory | 512 MB minimum; 1024 MB recommended with code functions |
| Timeout | 30 s minimum; match your slowest expected request |
| Package type | Image |
Then the environment variables:
| Variable | Required | What it does |
|---|---|---|
INSTANCEZ_OWNER_DATABASE_URL | Yes, or INSTANCEZ_DATABASE_URL | DSN for the instancez_owner role; runs migrations |
INSTANCEZ_AUTH_DATABASE_URL | Yes, or INSTANCEZ_DATABASE_URL | DSN for the authenticator role; serves requests |
INSTANCEZ_PUBLISHABLE_KEY | Yes | Client-safe key (inz_publishable_...), maps to anon |
INSTANCEZ_SECRET_KEY | Yes | Server-side key (inz_secret_...), maps to service_role |
INSTANCEZ_BUNDLE | With functions | s3://my-bucket/my-app/bundle.tar.gz |
INSTANCEZ_CONFIG | Without functions | s3://my-bucket/my-app/instancez.yaml |
INSTANCEZ_ENV_S3_BUCKET, INSTANCEZ_ENV_S3_REGION | For storage | Whatever your YAML references |
INSTANCEZ_BASE_URL | Recommended | Your public URL, used in auth email links |
The owner and authenticator pair is for databases where you don’t want to hand the function a superuser. A single superuser INSTANCEZ_DATABASE_URL also works, and instancez then creates the roles itself on first start. The environment variable reference has the details.
Grant the execution role s3:GetObject on the config or bundle object, plus read and write on the storage bucket. To pick up a new bundle without redeploying the function, set INSTANCEZ_WATCH=true and INSTANCEZ_WATCH_INTERVAL=60s. instancez then polls the S3 source, and calls already running finish on the old function workers.
Finally, give the function an HTTP front door, like a Function URL or API Gateway. Clients call that URL exactly as they would call Supabase:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-function-url', 'inz_publishable_your-key')
const { data } = await supabase.from('todos').select('title,status')
How do file uploads work on Lambda?
Lambda caps request payloads at 6 MB, and you don’t want big files streaming through a function anyway. With the S3 provider, instancez has a direct upload endpoint that returns a presigned S3 URL. The browser then sends the bytes straight to S3:
const { id, upload_url } = await fetch(`${apiUrl}/storage/avatars/sign`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ content_type: file.type, size: file.size }),
}).then(r => r.json())
// Upload directly to S3; Lambda is not in this path
await fetch(upload_url, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file })
The endpoint runs as the calling user, so the bucket’s insert RLS policy decides who may sign an upload. GET /storage/<bucket>/<id> returns a presigned download URL later. The storage docs have the full spec.
How did we test this without AWS?
We couldn’t run Lambda itself here, so we checked every piece around it. We built the image from Dockerfile.lambda and ran it with a read-only root filesystem and a writable /tmp, like Lambda. Next to it ran Postgres and an S3-compatible store. With inz bundle --output s3://... we uploaded the bundle, and the container loaded it via INSTANCEZ_BUNDLE. On start it applied migrations and answered 200 on /ready.
From there, supabase-js signed up a user, wrote and read a row under RLS, and called the code function. The direct upload flow above put a file in the bucket without the bytes touching the server. The ECR push and the function settings are copied from our docs, not run for this post.
How does Lambda compare to the other ways to run it?
It’s the same binary everywhere, so the choice is about operations, not features:
| AWS Lambda | VPS or Docker | Kubernetes | |
|---|---|---|---|
| What you run | A container function | inz serve on the host or in Compose | The Helm chart |
| Config source | S3 (INSTANCEZ_CONFIG or INSTANCEZ_BUNDLE) | Local file or bundle | ConfigMap |
| File storage | S3 | Local disk or S3 | S3 |
| When idle | Nothing running between requests | A server that’s always on | A pod that’s always on |
The self-hosting post walks through the VPS, Docker and Kubernetes columns. If you’d rather not run anything, instancez Cloud runs the same runtime and deploys with inz cloud deploy.
What are the trade-offs?
These apply wherever instancez runs, Lambda included:
- No Realtime or websocket subscriptions yet. It’s on the roadmap, built so it still works serverless. Until then, poll or refetch.
- instancez is designed to run as a single replica today, and Lambda can run several copies of a function at once. Multi-replica support is planned.
- Functions run on Node.js, so Deno Edge Functions from Supabase need porting.
- No phone or SMS login. OAuth covers Google, GitHub and Apple.
- Image resize handles JPEG and PNG, not WebP or AVIF.
- No database branching yet.
The comparison page has the full table, and Supabase alternatives puts it next to the other options. Moving an existing project? Start with the migration guide and switching from Supabase.
FAQ
Do I need to change my supabase-js code to use Lambda?
No. Point createClient at the function’s URL and use your publishable key. REST, auth, storage, RPC and functions.invoke calls stay the same.
Can I use the public ghcr image on Lambda?
Not directly. The -lambda tag is a multi-arch manifest list, and Lambda only accepts single-arch images from a private ECR repository. Build from Dockerfile.lambda and push to ECR.
Where should Postgres live? Anywhere the function can reach over the network, such as a managed Postgres in the same region. instancez only needs the DSNs from the table above.
Can I develop locally and then deploy the same project to Lambda?
Yes. Develop with inz dev --embedded-pg (local Supabase without Docker), then bundle and upload the same instancez.yaml and functions/.