Build a production-ready API backend using Supabase Edge Functions as RESTful endpoints in Lovable — no separate server needed. You'll get API key authentication, per-key rate limiting, an interactive endpoint docs DataTable, and a Card-based API key management dashboard — all in 2 hours.
What you're building
A Supabase Edge Function is a Deno-based serverless function that runs on Supabase's global edge network. Each function becomes a URL like https://your-project.supabase.co/functions/v1/function-name. With a shared authentication middleware that checks an api_keys table, you can expose a clean REST API without deploying a separate server.
Rate limiting works by recording each incoming request in a rate_limit_log table (api_key_id, timestamp). Before processing a request, the middleware counts rows for that key in the last 60 seconds. If the count exceeds the key's rate_limit value, the function returns a 429 response. Supabase handles concurrent requests safely with row-level INSERT.
The Lovable frontend is the developer dashboard for this API. It shows a DataTable of all endpoints with method, path, description, and example request/response in Tabs. The API key management section lets dashboard users create keys with custom scopes, set rate limits, and revoke keys. A request log table shows the last 500 API calls with status codes and response time.
Final result
A fully functional API backend with key auth, rate limiting, and a developer-facing dashboard — all without a custom server.
Tech stack
Prerequisites
- Lovable Pro account (multi-function Edge Function generation requires credits)
- Supabase project with Edge Functions enabled (free tier works)
- Supabase URL and service role key saved to Cloud tab → Secrets
- Basic familiarity with REST API concepts (GET, POST, JSON responses)
- Optional: an existing Supabase database with tables you want to expose via the API
Build steps
Set up the API management schema in Supabase
Prompt Lovable to create the tables that power the API key system and logging. These tables are internal to your backend — users of your API never interact with them directly. After generation, add your Supabase credentials to Cloud tab → Secrets.
1Create an API backend management system with Supabase. Set up these tables:23- api_keys: id, user_id (references auth.users), name, key_hash (text, unique), key_prefix (text, first 8 chars for display), scopes (text array: read|write|admin), rate_limit (int, requests per minute, default 60), is_active (bool default true), last_used_at, created_at4- rate_limit_log: id, api_key_id, endpoint, requested_at (timestamptz), response_status (int), response_time_ms (int)5- api_endpoints: id, method (GET|POST|PUT|DELETE|PATCH), path, description, request_schema (jsonb), response_schema (jsonb), is_public (bool), scopes_required (text array), created_at67RLS:8- api_keys: users can read/write their own keys only (user_id = auth.uid())9- rate_limit_log: service role only (no user-facing RLS policies)10- api_endpoints: public SELECT, admin-only INSERT/UPDATE/DELETE1112Generate a function generate_api_key() that creates a secure random key, stores its SHA-256 hash in key_hash, and returns the plaintext key ONCE (it is never stored in plaintext).Pro tip: Ask Lovable to also create a view api_keys_safe that returns all api_keys columns EXCEPT key_hash. Use this view in all frontend queries so the hash is never sent to the browser.
Expected result: All three tables are created. The generate_api_key() Supabase RPC function is ready. TypeScript types are generated. The preview shows the app shell.
Build the shared auth middleware Edge Function
Create a shared TypeScript module that all your API Edge Functions import. It reads the Authorization header, hashes the key, looks it up in api_keys, checks rate limit, and logs the request. This keeps auth logic in one place.
1Create a shared middleware module at supabase/functions/_shared/auth.ts.23The module should export an async function authenticateRequest(req: Request, requiredScope: string): Promise<{ apiKey: ApiKey } | Response>.45Logic:61. Read the Authorization header. Expect format: 'Bearer sk_YOUR_KEY'72. If missing, return new Response(JSON.stringify({ error: 'Missing API key' }), { status: 401 })83. Hash the provided key using SubtleCrypto SHA-25694. Query the api_keys table (using SUPABASE_SERVICE_ROLE_KEY from Deno.env) WHERE key_hash = hash AND is_active = true105. If not found, return 401 with { error: 'Invalid API key' }116. Check if requiredScope is in the key's scopes array. If not, return 403 { error: 'Insufficient scope' }127. Count rows in rate_limit_log WHERE api_key_id = key.id AND requested_at > now() - interval '1 minute'138. If count >= key.rate_limit, return 429 { error: 'Rate limit exceeded', retry_after: 60 }149. Insert a new row into rate_limit_log with api_key_id, endpoint, and requested_at = now()1510. Update api_keys.last_used_at = now() for this key1611. Return { apiKey: key }Pro tip: Store the Supabase service role key as SUPABASE_SERVICE_ROLE_KEY in Cloud tab → Secrets (not VITE_SUPABASE_SERVICE_ROLE_KEY — no VITE_ prefix for Edge Function secrets).
Expected result: The _shared/auth.ts module is created. All subsequent Edge Functions import it and call authenticateRequest at the top of their handler.
Create your first API endpoint Edge Function
Build an example Edge Function using the shared middleware. This shows the pattern every API endpoint follows: authenticate, parse input, query Supabase, return JSON. Customize the resource type to match your data.
1// supabase/functions/api-items/index.ts2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'4import { authenticateRequest } from '../_shared/auth.ts'56const corsHeaders = {7 'Access-Control-Allow-Origin': '*',8 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',9 'Content-Type': 'application/json',10}1112serve(async (req: Request) => {13 if (req.method === 'OPTIONS') {14 return new Response('ok', { headers: corsHeaders })15 }1617 const start = Date.now()18 const url = new URL(req.url)1920 try {21 const scope = req.method === 'GET' ? 'read' : 'write'22 const authResult = await authenticateRequest(req, scope)23 if (authResult instanceof Response) return authResult2425 const supabase = createClient(26 Deno.env.get('SUPABASE_URL') ?? '',27 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''28 )2930 if (req.method === 'GET') {31 const page = parseInt(url.searchParams.get('page') ?? '1')32 const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '20'), 100)33 const from = (page - 1) * limit3435 const { data, count, error } = await supabase36 .from('items')37 .select('*', { count: 'exact' })38 .range(from, from + limit - 1)3940 if (error) throw error4142 return new Response(JSON.stringify({43 data,44 meta: { page, limit, total: count },45 }), { headers: corsHeaders })46 }4748 if (req.method === 'POST') {49 const body = await req.json()50 const { data, error } = await supabase.from('items').insert(body).select().single()51 if (error) throw error52 return new Response(JSON.stringify({ data }), { status: 201, headers: corsHeaders })53 }5455 return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: corsHeaders })5657 } catch (err) {58 const message = err instanceof Error ? err.message : 'Internal server error'59 return new Response(JSON.stringify({ error: message }), { status: 500, headers: corsHeaders })60 }61})Pro tip: Add a response_time_ms to your rate_limit_log INSERT by calculating Date.now() - start before returning. This powers your latency metrics in the dashboard.
Expected result: The Edge Function deploys. Calling it with a valid API key in the Authorization header returns JSON. Calling without a key returns 401.
Build the API key management dashboard
Ask Lovable to create the dashboard page where users can create new API keys, see their key prefix and scopes, view last-used timestamps, and revoke keys they no longer need.
1Build an API key management page at src/pages/ApiKeys.tsx.23Requirements:4- Show existing keys from api_keys_safe view as Cards (not a table — cards show more info)5- Each Card shows: key name, key_prefix + '...' (e.g. 'sk_live_ab12...'), scopes as Badges, rate_limit, last_used_at relative time, and a 'Revoke' Button (destructive variant)6- Add a 'Create New Key' Button that opens a Dialog7- Dialog form fields (react-hook-form + zod):8 - Key name (required, text)9 - Scopes (Checkbox group: read, write, admin)10 - Rate limit (Select: 30/min, 60/min, 120/min, 300/min)11- On form submit, call the generate_api_key() Supabase RPC function12- After creation, show the full key in an Alert component with a copy-to-clipboard Button. Tell the user: 'Save this key now. It will not be shown again.'13- Revoking a key sets is_active = false via supabase.from('api_keys').update({ is_active: false })14- Show a usage Sparkline (mini bar chart) per card using Recharts showing last 7 days of request countsExpected result: The API keys page shows all user keys as Cards. Creating a key shows the full key once in an Alert. Revoking a key removes it from the list.
Build the endpoint documentation DataTable
Ask Lovable to build the interactive API docs page. A DataTable lists all endpoints from the api_endpoints table. Selecting a row expands Tabs showing the request schema, response schema, and a live test form.
1Build an API documentation page at src/pages/ApiDocs.tsx.23Requirements:4- Fetch all rows from api_endpoints table5- Render as a shadcn/ui DataTable (TanStack Table) with columns: Method Badge (GET=green, POST=blue, PUT=yellow, DELETE=red, PATCH=orange), Path (monospace font), Description, Scopes Required Badges, Is Public Badge6- Clicking a row expands an inline section below the row (not a separate page)7- Expanded section has three Tabs: 'Request', 'Response', 'Try It'8- Request tab: renders request_schema jsonb as a formatted JSON code block9- Response tab: renders response_schema jsonb as a formatted JSON code block10- Try It tab: shows a Textarea pre-filled with a curl command using the endpoint path, method, and a placeholder API key. Add a Copy button.11- Add a search Input above the table that filters by path or description12- Non-authenticated endpoints show a green 'Public' Badge instead of scopesExpected result: The docs page shows all endpoints in a filterable DataTable. Expanding a row reveals the three-tab detail view. The Try It tab shows a ready-to-copy curl command.
Complete code
1import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'23const corsHeaders = { 'Content-Type': 'application/json' }45async function hashKey(key: string): Promise<string> {6 const encoder = new TextEncoder()7 const data = encoder.encode(key)8 const hashBuffer = await crypto.subtle.digest('SHA-256', data)9 const hashArray = Array.from(new Uint8Array(hashBuffer))10 return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')11}1213type ApiKey = {14 id: string15 user_id: string16 scopes: string[]17 rate_limit: number18}1920export async function authenticateRequest(21 req: Request,22 requiredScope: string23): Promise<{ apiKey: ApiKey } | Response> {24 const authHeader = req.headers.get('Authorization')25 if (!authHeader?.startsWith('Bearer ')) {26 return new Response(JSON.stringify({ error: 'Missing API key' }), { status: 401, headers: corsHeaders })27 }2829 const rawKey = authHeader.slice(7)30 const hash = await hashKey(rawKey)3132 const supabase = createClient(33 Deno.env.get('SUPABASE_URL') ?? '',34 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''35 )3637 const { data: apiKey } = await supabase38 .from('api_keys')39 .select('id, user_id, scopes, rate_limit')40 .eq('key_hash', hash)41 .eq('is_active', true)42 .single()4344 if (!apiKey) {45 return new Response(JSON.stringify({ error: 'Invalid API key' }), { status: 401, headers: corsHeaders })46 }4748 if (!apiKey.scopes.includes(requiredScope) && !apiKey.scopes.includes('admin')) {49 return new Response(JSON.stringify({ error: 'Insufficient scope' }), { status: 403, headers: corsHeaders })50 }5152 const oneMinuteAgo = new Date(Date.now() - 60_000).toISOString()53 const { count } = await supabase54 .from('rate_limit_log')55 .select('*', { count: 'exact', head: true })56 .eq('api_key_id', apiKey.id)57 .gte('requested_at', oneMinuteAgo)5859 if ((count ?? 0) >= apiKey.rate_limit) {60 return new Response(JSON.stringify({ error: 'Rate limit exceeded', retry_after: 60 }), { status: 429, headers: corsHeaders })61 }6263 await Promise.all([64 supabase.from('rate_limit_log').insert({ api_key_id: apiKey.id, requested_at: new Date().toISOString() }),65 supabase.from('api_keys').update({ last_used_at: new Date().toISOString() }).eq('id', apiKey.id),66 ])6768 return { apiKey }69}Customization ideas
Versioned API endpoints
Add a version prefix to your Edge Function paths by creating separate function directories: api-v1-items, api-v2-items. Document both versions in the api_endpoints table with a version column. The docs DataTable lets users toggle between API versions using a Select at the top.
Webhook delivery system
Add a webhooks table where users register callback URLs for specific events. Create an Edge Function that fires when key database events occur (e.g. new item created) and delivers JSON payloads to registered URLs with HMAC signature headers for verification.
API analytics dashboard
Add a dedicated analytics page with Recharts charts: total requests per day over 30 days, top endpoints by call volume, error rate over time, and P95 response latency. All metrics come from aggregating the rate_limit_log table.
SDK code generation
Add a page that generates a TypeScript or Python SDK client based on your api_endpoints definitions. The page renders the generated code in a ScrollArea with syntax highlighting and a Download button. Ask Lovable to build a template-based code generator from the jsonb schemas.
IP allowlist per API key
Add an ip_allowlist column (text array) to api_keys. In the auth middleware, read the CF-Connecting-IP or X-Forwarded-For header and reject requests from IPs not in the list. The API keys dashboard shows an IP management section per key.
Common pitfalls
Pitfall: Using VITE_ prefix for Edge Function secrets
How to avoid: In Cloud tab → Secrets, store Edge Function secrets WITHOUT the VITE_ prefix (e.g. SUPABASE_SERVICE_ROLE_KEY, RESEND_API_KEY). Access them in Deno with Deno.env.get('SUPABASE_SERVICE_ROLE_KEY').
Pitfall: Storing the plaintext API key in the database
How to avoid: Store only the SHA-256 hash of the key. Show the plaintext key ONCE after generation and never retrieve it again. The generate_api_key() RPC function should return the key, store only the hash, and Lovable should show it in a one-time Alert dialog.
Pitfall: Not handling CORS preflight OPTIONS requests in Edge Functions
How to avoid: Add this at the top of every Edge Function handler: if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) }. Make sure corsHeaders includes Access-Control-Allow-Origin and Access-Control-Allow-Headers.
Pitfall: Querying rate_limit_log without an index on requested_at
How to avoid: Ask Lovable to add a SQL migration: CREATE INDEX idx_rate_limit_log_key_time ON rate_limit_log(api_key_id, requested_at DESC). Also add a scheduled Edge Function to delete rows older than 24 hours to keep the table small.
Best practices
- Hash API keys with SHA-256 before storage and never log the plaintext key anywhere. The one-time display on creation is the only time the user sees it.
- Keep the _shared/auth.ts middleware as the single source of truth for authentication. Never copy-paste auth logic into individual Edge Functions.
- Return consistent JSON error shapes from all endpoints: { error: string, code?: string }. This makes SDK generation and client-side error handling predictable.
- Add a version prefix to all your API paths (/v1/, /v2/) from day one. Changing API behavior without versioning breaks existing integrations.
- Log response_time_ms in every rate_limit_log INSERT so you have latency data for free. Calculate it as Date.now() - requestStartTime just before returning the response.
- Use SECURITY DEFINER Supabase functions for key generation so the hash logic runs with elevated permissions even when called by the anon key. Wrap the function in a security wrapper.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a REST API with Supabase Edge Functions. I have a shared auth middleware that validates API keys by hashing and looking up in a Supabase table. Help me design a rate limiting approach that works with Deno and Supabase PostgreSQL. I want to limit per key per minute. Should I use a sliding window or fixed window counter? Show me the SQL schema for the rate_limit_log table and the TypeScript code for the count-and-insert check inside the Deno Edge Function.
Add a request log page at /logs. Fetch the last 500 rows from rate_limit_log joined with api_keys to show the key name. Render as a shadcn/ui DataTable with columns: timestamp (relative), API key name, endpoint (monospace), response status (Badge: 2xx=green, 4xx=yellow, 5xx=red), response time ms. Add filters: status code range (2xx/4xx/5xx), API key Select, and a date range Popover. Refresh automatically every 30 seconds.
In Supabase, create a scheduled Edge Function that runs every hour. It should delete rows from rate_limit_log where requested_at < now() - interval '24 hours'. Also delete rows where requested_at < now() - interval '90 days' from any audit_log table. Return a JSON response with { deleted_rate_limit_rows: count, deleted_audit_rows: count } so the Supabase logs show what was cleaned.
Frequently asked questions
Are Supabase Edge Functions really production-ready as an API layer?
Yes. Edge Functions run on Deno Deploy's global edge network with low latency, automatic scaling, and 99.9% uptime SLAs on paid plans. They support all standard web APIs and npm packages via esm.sh. Many production applications use them as their primary API layer with thousands of requests per second.
How do I deploy Edge Functions when I make changes in Lovable?
Lovable deploys Edge Functions automatically when you publish your app. You can also trigger a manual deploy by clicking the Publish icon (top-right) at any time. Each Edge Function in your supabase/functions/ directory is deployed as a separate endpoint. Check the Cloud tab → Edge Functions for deployment status and logs.
What is the rate limit on Supabase Edge Functions themselves?
Supabase Free plan allows 500,000 Edge Function invocations per month. Pro plan ($25/mo) allows 2,000,000. Beyond that, usage-based pricing applies. This is separate from the per-API-key rate limits you implement in your own code. For high-traffic APIs, upgrade to Supabase Pro and consider caching responses where appropriate.
Can I use this pattern to expose a public API that other developers can integrate?
Yes, that is exactly the use case. The generate_api_key flow creates keys you can share with third-party developers. The scopes system (read, write, admin) lets you issue read-only keys to partners and write keys to trusted integrations. Add the docs page so external developers can explore the API without needing access to your codebase.
How do I handle authentication for users of my app separately from API key authentication?
The API key system is for programmatic access by other systems. Human users of your Lovable dashboard use Supabase Auth (email/password or OAuth). These are two separate layers: Supabase Auth sessions control who can manage keys in the dashboard, while API key Bearer tokens control who can call the Edge Function endpoints. They share the same Supabase project but operate independently.
Can I test Edge Functions locally before publishing?
Lovable does not provide a local development environment — everything runs in the browser. To test Edge Functions before publishing, you can use Lovable's Plan Mode to review the function code, then publish and test against the live Edge Function URL using the Try It tab in your API docs page or an external tool like Postman.
Is there help available for building a more complex API backend?
RapidDev builds production Lovable apps including API backends with custom authentication, multi-tenant isolation, and complex business logic. Reach out if you need an API that goes beyond the pattern in this guide.
How do I add input validation to my Edge Functions?
In Deno Edge Functions, parse the request body with await req.json() and validate it against a schema. For TypeScript type safety, use a lightweight validation library compatible with Deno like Zod (importable via esm.sh). Return a 400 response with field-level error details if validation fails: { error: 'Validation failed', fields: { name: 'Required' } }.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation