Skip to main content

Edge Function Contract

NaijaBase Edge Functions run in isolated Node.js 20 containers. The contract is intentionally simple.

The handler signature

async function handler(payload) {
return { result: 'value' }
}

payload is the parsed request body. Return any plain value — it becomes the response.

Calling your function

NaijaBase supports two URL patterns — both are equivalent:

# Pattern A — canonical (top-level)
curl -X POST \
https://api.naijabase.dev/functions/v1/PROJECT_ID/my-function \
-H "Authorization: Bearer YOUR_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Duro", "amount": 5000}'

# Pattern B — project-scoped (Supabase-compatible)
curl -X POST \
https://api.naijabase.dev/projects/PROJECT_ID/functions/v1/my-function \
-H "Authorization: Bearer YOUR_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Duro", "amount": 5000}'

Use Pattern A when you have a standalone NAIJABASE_BASE_URL env var. Use Pattern B when your NAIJABASE_URL already includes the project path (e.g. https://api.naijabase.dev/projects/{id}) — this is the default for projects bootstrapped from the NaijaBase dashboard.

Making HTTP calls

Use the built-in fetch() — available in Node 20:

async function handler({ amount, currency }) {
const response = await fetch(
'https://api.exchangerate.host/convert?' +
`from=USD&to=${currency}&amount=${amount}`
)
const data = await response.json()
return { converted: data.result }
}

Calling the NaijaBase REST API

Since the SDK is not available inside functions, call your project's REST API directly via fetch:

async function handler({ userId }) {
const PROJECT_ID = 'your-project-id'
const SERVICE_KEY = 'nb_service_xxxx'

const response = await fetch(
`https://api.naijabase.dev/projects/${PROJECT_ID}/rest/v1/users?id=eq.${userId}`,
{
headers: {
'apikey': SERVICE_KEY,
'Authorization': `Bearer ${SERVICE_KEY}`
}
}
)
const users = await response.json()
return { user: users[0] }
}

Paystack payment

async function handler({ email, amount }) {
const response = await fetch(
'https://api.paystack.co/transaction/initialize',
{
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_YOUR_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
amount: amount * 100 // kobo
})
}
)
const data = await response.json()
return { checkout_url: data.data.authorization_url }
}

Termii SMS

async function handler({ phone, message }) {
const response = await fetch(
'https://api.ng.termii.com/api/sms/send',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: phone,
from: 'YourApp',
sms: message,
type: 'plain',
api_key: 'YOUR_TERMII_KEY',
channel: 'generic'
})
}
)
const data = await response.json()
return { message_id: data.message_id }
}

What is available

FeatureAvailable
fetch()✅ Yes — Node 20 built-in
Node.js built-ins (crypto, buffer, url)✅ Yes
payload (request body)✅ Yes
npm packages❌ No
process.env❌ No — pass secrets in payload
File system access❌ No
@naijabase/js SDK❌ No — use fetch() instead

What is NOT available

// ❌ These will fail
import { createClient } from '@naijabase/js'
const axios = require('axios')
process.env.MY_SECRET
require('child_process')

Compared to Supabase Edge Functions

If you are migrating from Supabase, here are the key differences:

SupabaseNaijaBase
Deno runtimeNode.js 20
serve(async (req) => {...})async function handler(payload) {...}
await req.json()payload directly
req.headers.get('x')Pass in payload body
Deno.env.get('KEY')Pass secrets in payload
import from 'https://esm.sh/...'Use fetch()
new Response(...)return plainObject
Supabase SDK availableUse fetch() to REST API

Calling Edge Functions from a Next.js app

The security rule

Never call Edge Functions with your service key from the browser. Your service key bypasses all Row Level Security — exposing it in browser code gives anyone full access to your entire database.

The correct pattern

Use Next.js API routes as a server-side proxy:

Browser → /api/your-route → NaijaBase Edge Function

The service key lives only in your server environment, never in the browser.

Step 1 — Create a shared helper

Create lib/naijabase-functions.ts:

// NAIJABASE_URL = https://api.naijabase.dev/projects/{projectId}
// Builds: .../projects/{projectId}/functions/v1/{name} ← Pattern B
// NaijaBase supports both /functions/v1/{projectId}/{name} and this form.
const NAIJABASE_URL = process.env.NAIJABASE_URL
const SERVICE_KEY = process.env.NAIJABASE_SERVICE_KEY

export async function callFunction(
name: string,
payload: Record<string, unknown>
) {
const res = await fetch(
`${NAIJABASE_URL}/functions/v1/${name}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${SERVICE_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
)

const data = await res.json()
if (!res.ok) {
throw new Error(data.error || `Function ${name} failed`)
}
return data
}

Step 2 — Create a Next.js API route

// app/api/send-welcome-email/route.ts
// This runs on the server — service key is safe here

import { callFunction } from '@/lib/naijabase-functions'

export async function POST(req: Request) {
const { email, name } = await req.json()
try {
const data = await callFunction(
'send-welcome-email',
{ email, name }
)
return Response.json(data)
} catch (err: any) {
return Response.json(
{ error: err.message },
{ status: 500 }
)
}
}

Step 3 — Call from the browser

// Any client component — no keys needed
const res = await fetch('/api/send-welcome-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, name })
})
const data = await res.json()

What goes where

OperationCall fromKey used
Read data with RLSBrowserAnon key
Write data with RLSBrowserAnon key
Send email or SMSNext.js /api/ routeService key
Process paymentNext.js /api/ routeService key
Admin operationsNext.js /api/ routeService key
Edge FunctionsNext.js /api/ routeService key

Environment variables

# .env.local

# Safe to expose in browser (NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_NAIJABASE_URL=https://api.naijabase.dev/projects/YOUR_ID
NEXT_PUBLIC_NAIJABASE_ANON_KEY=nb_anon_xxx

# Never expose — server-side only (no NEXT_PUBLIC_)
NAIJABASE_SERVICE_KEY=nb_service_xxx
NAIJABASE_JWT_SECRET=xxx

Verify no keys are leaking

# Should return zero results
grep -r "SERVICE_KEY" src/
grep -r "nb_service_" src/

If any results appear — move that code into an /api/ route immediately.


Compared to Supabase

The same pattern applies when using Supabase. This is not a NaijaBase limitation — it is standard security practice for all BaaS platforms.

NaijaBaseSupabase
Anon key in browserSafe with RLSSafe with RLS
Service key in browserNeverNever
Edge Functions from browserVia /api/ proxyVia /api/ proxy
Secrets in functionsproject_secrets tableSupabase Vault