name: supabase-edge-functions-esm-sh-fix
description: Fix BOOT_ERROR on Supabase Edge Functions caused by external imports (esm.sh, unpkg, etc.) when the native Deno fetch and Supabase Auth REST API can be used instead
category: devops
Supabase Edge Functions — BOOT_ERROR from External Imports
Symptoms
- - Edge Function returns
{"code":"BOOT_ERROR","message":"Function failed to start (please check logs)"}on every call - - CORS preflight returns BOOT_ERROR (misleading — appears as CORS issue in browser console)
- -
curl -X OPTIONSon the function URL returns BOOT_ERROR, not CORS headers - - The function worked before or works in other projects
- -
.eq("col","val")→col=eq.val - -
.neq("col","val")→col=neq.val - -
.is("col", null)→col=is.null - -
.not("col", "is", null)→col=not.is.null - -
.in("col",[a,b])→col=in.(a,b) - -
.ilike("col","%val%")→col=ilike.%val% - -
.lt("col","val")→col=lt.val - -
.lte("col","val")→col=lte.val - -
.gte("col","val")→col=gte.val - -
.order("col",{ascending:false})→order=col.desc - -
.limit(N)→limit=N - -
.range(from, to)→offset=${from}&limit=${to - from + 1} - -
select("")→select= - -
.maybeSingle()→ result is array, checkdata?.[0] - - The ACTIVE volume is
/opt/rochasales/supabase/functions(mounted to functions container) - - But
/opt/rochasales/volumes/functions/manage-users/index.tsis a SEPARATE old copy (not mounted) - - Always verify code at
/opt/rochasales/supabase/functions/after updates
Root Cause
The function imports @supabase/supabase-js or zod (or any other package) from esm.sh, unpkg, jsdelivr, or similar CDNs. Supabase Cloud Edge Functions run in a Deno runtime that may not have network access to these CDNs, or the CDN may be blocked/slow at the Edge.
The fix is NOT to add CORS headers — the function never starts, so CORS headers from the function body are never returned.
Diagnosis
`bash
Test from your terminal — esm.sh might be reachable from your machine
but NOT from Supabase's Edge Runtime servers
curl -s --max-time 15 "https://esm.sh/@supabase/supabase-js@2.57.4" -I | head -5
Call the function directly — if BOOT_ERROR, the function body never executed
curl -s --max-time 15 -X POST "https://YOUR_PROJECT.supabase.co/functions/v1/YOUR_FUNCTION" \
-H "Content-Type: application/json" \
-H "apikey: YOUR_ANON_KEY" \
-d '{}'
→ {"code":"BOOT_ERROR","message":"Function failed to start (please check logs)"}
Test via OPTIONS (CORS preflight) — same BOOT_ERROR confirms function never started
curl -s --max-time 15 -X OPTIONS "https://YOUR_PROJECT.supabase.co/functions/v1/YOUR_FUNCTION" \
-H "Origin: https://your-frontend.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,apikey,content-type"
`
Fix — Rewrite Without External Imports
Two approaches: Minimal (Auth only) vs Full (Auth + Database)
#### Approach 1: Minimal — Pure Auth REST calls (no SDK)
For functions that only touch auth (reset password, update email):
`typescript
// ❌ BEFORE
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
// ✅ AFTER — pure Deno, no external imports
async function authUpdateUser(serviceRoleKey: string, supabaseUrl: string, userId: string, attrs: Record
const res = await fetch(${supabaseUrl}/auth/v1/admin/users/${userId}, {
method: 'PUT',
headers: { Authorization: Bearer ${serviceRoleKey}, apikey: serviceRoleKey, 'Content-Type': 'application/json' },
body: JSON.stringify(attrs),
});
return { data: await res.json(), error: res.ok ? null : await res.text(), status: res.status };
}
`
#### Approach 2: Full createAdminClient() — Auth + Database in one client
This is the recommended pattern for functions that need both auth AND database access. Uses cross-fetch (which IS bundled in Deno, NOT external):
`typescript
// ❌ BEFORE
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
// ✅ AFTER — uses cross-fetch which is bundled in Deno runtime (no external fetch needed)
import { fetch as crossFetch } from 'https://esm.sh/cross-fetch@4.0.0';
function createAdminClient() {
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const serviceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
return {
auth: {
admin: {
updateUserById: (userId: string, attributes: Record
fetch(${supabaseUrl}/auth/v1/admin/users/${userId}, {
method: 'PUT',
headers: { Authorization: Bearer ${serviceKey}, apikey: serviceKey, 'Content-Type': 'application/json' },
body: JSON.stringify(attributes),
}).then(r => r.json()),
listUsers: () =>
fetch(${supabaseUrl}/auth/v1/admin/users, {
headers: { Authorization: Bearer ${serviceKey}, apikey: serviceKey },
}).then(r => r.json()),
},
},
from: (table: string) => ({
select: (cols: string) => ({
eq: (col: string, val: string) => ({
limit: (n: number) => ({
then: (resolve: Function, reject: Function) => {
const q = new URLSearchParams({ [col]: eq.${val}, select: cols, limit: String(n) });
fetch(${supabaseUrl}/rest/v1/${table}?${q.toString()}, {
headers: { Authorization: Bearer ${serviceKey}, apikey: serviceKey },
}).then(async r => {
const data = await r.json();
resolve({ data: Array.isArray(data) ? data : data ? [data] : [], error: r.ok ? null : data });
}).catch(reject);
},
}),
}),
}),
insert: (rows: Record
then: (resolve: Function, reject: Function) => {
fetch(${supabaseUrl}/rest/v1/${table}, {
method: 'POST',
headers: { Authorization: Bearer ${serviceKey}, apikey: serviceKey, 'Content-Type': 'application/json', Prefer: 'return=minimal' },
body: JSON.stringify(rows),
}).then(async r => {
const txt = await r.text();
resolve({ error: r.ok ? null : txt, status: r.status });
}).catch(reject);
},
}),
}),
};
}
`
Then in the handler:
`typescript
const supabase = createAdminClient();
const { data } = await supabase.from('profiles').select('id').eq('user_id', userId).limit(1);
// data is the array result
`
#### Approach 3: REST helpers (for complex functions with many DB calls)
For functions with 10+ database calls, use structured helper functions:
`typescript
// ── REST helpers (drop-in replacement for supabase client) ──
async function sbSelect(url: string, key: string, table: string, params: Record
const q = new URLSearchParams(params);
const res = await fetch(${url}/rest/v1/${table}?${q.toString()}, {
headers: { Authorization: Bearer ${key}, apikey: key },
});
const data = await res.json();
return { data: Array.isArray(data) ? data : data ? [data] : [], error: res.ok ? null : data };
}
async function sbInsert(url: string, key: string, table: string, rows: Record
const res = await fetch(${url}/rest/v1/${table}, {
method: 'POST',
headers: { Authorization: Bearer ${key}, apikey: key, 'Content-Type': 'application/json', Prefer: 'return=minimal' },
body: JSON.stringify(rows),
});
const txt = await res.text();
return { error: res.ok ? null : txt, status: res.status };
}
async function sbUpdate(url: string, key: string, table: string, updates: Record
const q = new URLSearchParams(Object.entries(match).map(([k,v]) => [k, eq.${v}]));
const res = await fetch(${url}/rest/v1/${table}?${q.toString()}, {
method: 'PATCH',
headers: { Authorization: Bearer ${key}, apikey: key, 'Content-Type': 'application/json', Prefer: 'return=minimal' },
body: JSON.stringify(updates),
});
const txt = await res.text();
return { error: res.ok ? null : txt, status: res.status };
}
async function sbUpsert(url: string, key: string, table: string, rows: Record
const res = await fetch(${url}/rest/v1/${table}, {
method: 'POST',
headers: { Authorization: Bearer ${key}, apikey: key, 'Content-Type': 'application/json', Prefer: 'resolution=merge' },
body: JSON.stringify(rows),
});
return { data: await res.json().catch(() => null), error: res.ok ? null : res.statusText, status: res.status };
}
`
URL param format reference (Supabase PostgREST):
Auth REST API endpoints available (no SDK needed)
| Action | Endpoint | ||
| -------- | ---------- | ||
| Get user | GET /auth/v1/admin/users/{user_id} | ||
| Update user | PUT /auth/v1/admin/users/{user_id} | ||
| List users | GET /auth/v1/admin/users | ||
| Create user | POST /auth/v1/admin/users | ||
| Delete user | DELETE /auth/v1/admin/users/{user_id} | ||
| Validate JWT | GET /auth/v1/user (requires Bearer token + anon key in headers) | ||
| Check role (via RPC) | POST /rest/v1/rpc/has_role | ||
| Function | Pattern | Lines | Notes |
| ---------- | --------- | ------- | ------- |
admin-reset-user-password | Minimal REST | ~100 | Pure fetch to Auth API |
admin-update-user-email | createAdminClient() | ~167 | Auth + DB via REST helpers |
dispatch-automation | REST helpers | ~400 | Complex event routing |
crm-lead-sync | External client + REST | ~400 | EXTERNAL supabase client kept (different project) |
sales-overdue-alerts | REST helpers | ~250 | Cron job pattern |
test-broadcast | REST helpers | ~100 | Simple broadcast |
invite-member | REST helpers | ~150 | Auth user creation + team_members |
register-uazapi-webhook | Minimal REST | ~100 | Pure UAZAPI API call |
uazapi-webhook | REST helpers | ~719 | Most complex — many DB calls |
resend-pending-meetings | REST helpers | ~210 | Task grouping + dedup |
send-reminders | REST helpers | ~500+ | Complex multi-automation types |
crm-mirror-health | External client + REST | ~166 | EXTERNAL supabase client kept |
| Service | Container | Port | Notes |
| --------- | ----------- | ------ | ------- |
| kong | deploy-vps-kong-1 | 8000 | Routes /functions/v1 → functions:9000 |
| functions | deploy-vps-functions-1 | 9000 (internal) | Edge Runtime — can be DOWN |
| rest | deploy-vps-rest-1 | 3002 | PostgREST |
| auth | deploy-vps-auth-1 | 9999 | Gotrue |
Functions volume mount: /opt/rochasales/supabase/functions → /home/deno/functions
Functions entry point: start --main-service /home/deno/functions/main
Functions is a named service — it CAN be missing while Kong+rest+auth run fine.
If functions container is down, Kong returns generic proxy errors, NOT BOOT_ERROR.
/opt/rochasales/docker-compose.yml — SEPARATE project (not fully used)
This has a functions section but no functions container is actually running from it.
Its volumes/functions only has manage-users/ (no main/), and the docker-compose services don't match the running containers.
Diagnosing self-hosted functions issues
`bash
Check which docker-compose project controls which containers
cd /docker/deploy-vps && docker compose ps # functions service here
cd /opt/rochasales && docker compose ps # different project, functions not running
Check if functions container exists
docker ps -a --filter name=functions
Check Kong routing
curl -s http://localhost:8000/functions/v1/health
If returns proxy error → functions container down
If returns BOOT_ERROR → functions container running but code has esm.sh import issue
Check what Kong routes to functions
docker exec deploy-vps-kong-1 cat /tmp/kong.yml | grep -A5 functions
`
Restarting functions in self-hosted Supabase
`bash
cd /docker/deploy-vps
docker compose up -d functions
docker compose ps functions
docker logs deploy-vps-functions-1 --tail 20
`
Common self-hosted issue: old esm.sh code in volumes
If you updated code at /opt/rochasales/supabase/functions/ but the functions still fail:
Key Insight
Browser console shows CORS error, but the root cause is BOOT_ERROR on the server. The browser's CORS preflight fails because the function body never executed — it returned an error before the CORS headers were set. Always test the function directly with curl first, not from the browser.
On self-hosted: BOOT_ERROR = function container running but code has import issue. Proxy error = function container not running at all.
Testing auth functions — When testing admin-reset-user-password or admin-update-user-email, you need a real JWT from a login session. The anon key alone is NOT enough for auth validation. Get a token via:
`bash
TOKEN=$(curl -s -X POST "https://PROJECT.supabase.co/auth/v1/token?grant_type=password" \
-H "apikey: $ANON_KEY" -H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"PASS"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
`