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
| Feature | Available |
|---|---|
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:
| Supabase | NaijaBase |
|---|---|
| Deno runtime | Node.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 available | Use 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
| Operation | Call from | Key used |
|---|---|---|
| Read data with RLS | Browser | Anon key |
| Write data with RLS | Browser | Anon key |
| Send email or SMS | Next.js /api/ route | Service key |
| Process payment | Next.js /api/ route | Service key |
| Admin operations | Next.js /api/ route | Service key |
| Edge Functions | Next.js /api/ route | Service 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.
| NaijaBase | Supabase | |
|---|---|---|
| Anon key in browser | Safe with RLS | Safe with RLS |
| Service key in browser | Never | Never |
| Edge Functions from browser | Via /api/ proxy | Via /api/ proxy |
| Secrets in functions | project_secrets table | Supabase Vault |