Skip to main content
RapidDev - Software Development Agency

How to Build a API Backend with Lovable

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'll build

  • Supabase Edge Functions as versioned RESTful API endpoints with JSON responses
  • API key authentication middleware shared across all Edge Functions
  • Rate limiting via a rate_limit_log table that tracks requests per key per window
  • Interactive endpoint documentation DataTable with request and response Tabs
  • API key management Cards with create, revoke, and usage stats per key
  • API key scopes (read, write, admin) enforced in the auth middleware
  • Request logging dashboard showing recent calls, status codes, and latency
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read2–2.5 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

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

LovableFrontend dashboard
Supabase Edge FunctionsAPI endpoints (Deno)
SupabaseDatabase
Supabase AuthDashboard auth
shadcn/uiUI Components
TanStack Table v8Endpoint docs DataTable

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

1

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.

prompt.txt
1Create an API backend management system with Supabase. Set up these tables:
2
3- 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_at
4- 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_at
6
7RLS:
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/DELETE
11
12Generate 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.

2

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.

prompt.txt
1Create a shared middleware module at supabase/functions/_shared/auth.ts.
2
3The module should export an async function authenticateRequest(req: Request, requiredScope: string): Promise<{ apiKey: ApiKey } | Response>.
4
5Logic:
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-256
94. Query the api_keys table (using SUPABASE_SERVICE_ROLE_KEY from Deno.env) WHERE key_hash = hash AND is_active = true
105. 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 key
1611. 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.

3

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.

supabase/functions/api-items/index.ts
1// supabase/functions/api-items/index.ts
2import { 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'
5
6const corsHeaders = {
7 'Access-Control-Allow-Origin': '*',
8 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
9 'Content-Type': 'application/json',
10}
11
12serve(async (req: Request) => {
13 if (req.method === 'OPTIONS') {
14 return new Response('ok', { headers: corsHeaders })
15 }
16
17 const start = Date.now()
18 const url = new URL(req.url)
19
20 try {
21 const scope = req.method === 'GET' ? 'read' : 'write'
22 const authResult = await authenticateRequest(req, scope)
23 if (authResult instanceof Response) return authResult
24
25 const supabase = createClient(
26 Deno.env.get('SUPABASE_URL') ?? '',
27 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
28 )
29
30 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) * limit
34
35 const { data, count, error } = await supabase
36 .from('items')
37 .select('*', { count: 'exact' })
38 .range(from, from + limit - 1)
39
40 if (error) throw error
41
42 return new Response(JSON.stringify({
43 data,
44 meta: { page, limit, total: count },
45 }), { headers: corsHeaders })
46 }
47
48 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 error
52 return new Response(JSON.stringify({ data }), { status: 201, headers: corsHeaders })
53 }
54
55 return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: corsHeaders })
56
57 } 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.

4

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.

prompt.txt
1Build an API key management page at src/pages/ApiKeys.tsx.
2
3Requirements:
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 Dialog
7- 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 function
12- 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 counts

Expected 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.

5

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.

prompt.txt
1Build an API documentation page at src/pages/ApiDocs.tsx.
2
3Requirements:
4- Fetch all rows from api_endpoints table
5- 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 Badge
6- 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 block
9- Response tab: renders response_schema jsonb as a formatted JSON code block
10- 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 description
12- Non-authenticated endpoints show a green 'Public' Badge instead of scopes

Expected 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

supabase/functions/_shared/auth.ts
1import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
2
3const corsHeaders = { 'Content-Type': 'application/json' }
4
5async 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}
12
13type ApiKey = {
14 id: string
15 user_id: string
16 scopes: string[]
17 rate_limit: number
18}
19
20export async function authenticateRequest(
21 req: Request,
22 requiredScope: string
23): 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 }
28
29 const rawKey = authHeader.slice(7)
30 const hash = await hashKey(rawKey)
31
32 const supabase = createClient(
33 Deno.env.get('SUPABASE_URL') ?? '',
34 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
35 )
36
37 const { data: apiKey } = await supabase
38 .from('api_keys')
39 .select('id, user_id, scopes, rate_limit')
40 .eq('key_hash', hash)
41 .eq('is_active', true)
42 .single()
43
44 if (!apiKey) {
45 return new Response(JSON.stringify({ error: 'Invalid API key' }), { status: 401, headers: corsHeaders })
46 }
47
48 if (!apiKey.scopes.includes(requiredScope) && !apiKey.scopes.includes('admin')) {
49 return new Response(JSON.stringify({ error: 'Insufficient scope' }), { status: 403, headers: corsHeaders })
50 }
51
52 const oneMinuteAgo = new Date(Date.now() - 60_000).toISOString()
53 const { count } = await supabase
54 .from('rate_limit_log')
55 .select('*', { count: 'exact', head: true })
56 .eq('api_key_id', apiKey.id)
57 .gte('requested_at', oneMinuteAgo)
58
59 if ((count ?? 0) >= apiKey.rate_limit) {
60 return new Response(JSON.stringify({ error: 'Rate limit exceeded', retry_after: 60 }), { status: 429, headers: corsHeaders })
61 }
62
63 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 ])
67
68 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.

ChatGPT Prompt

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.

Lovable Prompt

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.

Build Prompt

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' } }.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.