# How to Build a API Backend with Lovable

- Tool: How to Build with Lovable
- Difficulty: Intermediate
- Compatibility: Lovable Pro or higher
- Last updated: April 2026

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

## Before you start

- 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

## Step-by-step guide

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

```
Create an API backend management system with Supabase. Set up these tables:

- 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
- rate_limit_log: id, api_key_id, endpoint, requested_at (timestamptz), response_status (int), response_time_ms (int)
- 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

RLS:
- api_keys: users can read/write their own keys only (user_id = auth.uid())
- rate_limit_log: service role only (no user-facing RLS policies)
- api_endpoints: public SELECT, admin-only INSERT/UPDATE/DELETE

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

```
Create a shared middleware module at supabase/functions/_shared/auth.ts.

The module should export an async function authenticateRequest(req: Request, requiredScope: string): Promise<{ apiKey: ApiKey } | Response>.

Logic:
1. Read the Authorization header. Expect format: 'Bearer sk_YOUR_KEY'
2. If missing, return new Response(JSON.stringify({ error: 'Missing API key' }), { status: 401 })
3. Hash the provided key using SubtleCrypto SHA-256
4. Query the api_keys table (using SUPABASE_SERVICE_ROLE_KEY from Deno.env) WHERE key_hash = hash AND is_active = true
5. If not found, return 401 with { error: 'Invalid API key' }
6. Check if requiredScope is in the key's scopes array. If not, return 403 { error: 'Insufficient scope' }
7. Count rows in rate_limit_log WHERE api_key_id = key.id AND requested_at > now() - interval '1 minute'
8. If count >= key.rate_limit, return 429 { error: 'Rate limit exceeded', retry_after: 60 }
9. Insert a new row into rate_limit_log with api_key_id, endpoint, and requested_at = now()
10. Update api_keys.last_used_at = now() for this key
11. 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
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { authenticateRequest } from '../_shared/auth.ts'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Content-Type': 'application/json',
}

serve(async (req: Request) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  const start = Date.now()
  const url = new URL(req.url)

  try {
    const scope = req.method === 'GET' ? 'read' : 'write'
    const authResult = await authenticateRequest(req, scope)
    if (authResult instanceof Response) return authResult

    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
    )

    if (req.method === 'GET') {
      const page = parseInt(url.searchParams.get('page') ?? '1')
      const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '20'), 100)
      const from = (page - 1) * limit

      const { data, count, error } = await supabase
        .from('items')
        .select('*', { count: 'exact' })
        .range(from, from + limit - 1)

      if (error) throw error

      return new Response(JSON.stringify({
        data,
        meta: { page, limit, total: count },
      }), { headers: corsHeaders })
    }

    if (req.method === 'POST') {
      const body = await req.json()
      const { data, error } = await supabase.from('items').insert(body).select().single()
      if (error) throw error
      return new Response(JSON.stringify({ data }), { status: 201, headers: corsHeaders })
    }

    return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: corsHeaders })

  } catch (err) {
    const message = err instanceof Error ? err.message : 'Internal server error'
    return new Response(JSON.stringify({ error: message }), { status: 500, headers: corsHeaders })
  }
})
```

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

```
Build an API key management page at src/pages/ApiKeys.tsx.

Requirements:
- Show existing keys from api_keys_safe view as Cards (not a table — cards show more info)
- 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)
- Add a 'Create New Key' Button that opens a Dialog
- Dialog form fields (react-hook-form + zod):
  - Key name (required, text)
  - Scopes (Checkbox group: read, write, admin)
  - Rate limit (Select: 30/min, 60/min, 120/min, 300/min)
- On form submit, call the generate_api_key() Supabase RPC function
- 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.'
- Revoking a key sets is_active = false via supabase.from('api_keys').update({ is_active: false })
- 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.

```
Build an API documentation page at src/pages/ApiDocs.tsx.

Requirements:
- Fetch all rows from api_endpoints table
- 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
- Clicking a row expands an inline section below the row (not a separate page)
- Expanded section has three Tabs: 'Request', 'Response', 'Try It'
- Request tab: renders request_schema jsonb as a formatted JSON code block
- Response tab: renders response_schema jsonb as a formatted JSON code block
- 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.
- Add a search Input above the table that filters by path or description
- 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 example

File: `supabase/functions/_shared/auth.ts`

```typescript
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = { 'Content-Type': 'application/json' }

async function hashKey(key: string): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(key)
  const hashBuffer = await crypto.subtle.digest('SHA-256', data)
  const hashArray = Array.from(new Uint8Array(hashBuffer))
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}

type ApiKey = {
  id: string
  user_id: string
  scopes: string[]
  rate_limit: number
}

export async function authenticateRequest(
  req: Request,
  requiredScope: string
): Promise<{ apiKey: ApiKey } | Response> {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader?.startsWith('Bearer ')) {
    return new Response(JSON.stringify({ error: 'Missing API key' }), { status: 401, headers: corsHeaders })
  }

  const rawKey = authHeader.slice(7)
  const hash = await hashKey(rawKey)

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
  )

  const { data: apiKey } = await supabase
    .from('api_keys')
    .select('id, user_id, scopes, rate_limit')
    .eq('key_hash', hash)
    .eq('is_active', true)
    .single()

  if (!apiKey) {
    return new Response(JSON.stringify({ error: 'Invalid API key' }), { status: 401, headers: corsHeaders })
  }

  if (!apiKey.scopes.includes(requiredScope) && !apiKey.scopes.includes('admin')) {
    return new Response(JSON.stringify({ error: 'Insufficient scope' }), { status: 403, headers: corsHeaders })
  }

  const oneMinuteAgo = new Date(Date.now() - 60_000).toISOString()
  const { count } = await supabase
    .from('rate_limit_log')
    .select('*', { count: 'exact', head: true })
    .eq('api_key_id', apiKey.id)
    .gte('requested_at', oneMinuteAgo)

  if ((count ?? 0) >= apiKey.rate_limit) {
    return new Response(JSON.stringify({ error: 'Rate limit exceeded', retry_after: 60 }), { status: 429, headers: corsHeaders })
  }

  await Promise.all([
    supabase.from('rate_limit_log').insert({ api_key_id: apiKey.id, requested_at: new Date().toISOString() }),
    supabase.from('api_keys').update({ last_used_at: new Date().toISOString() }).eq('id', apiKey.id),
  ])

  return { apiKey }
}
```

## Common mistakes

- **Using VITE_ prefix for Edge Function secrets** — VITE_ environment variables are compiled into the browser bundle at build time. They are not available in Deno Edge Function runtime and would expose secrets to the public. Fix: 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').
- **Storing the plaintext API key in the database** — If your database is ever accessed by an unauthorized party, all user keys are immediately compromised. Fix: 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.
- **Not handling CORS preflight OPTIONS requests in Edge Functions** — Browsers send an OPTIONS preflight before cross-origin POST requests. If the Edge Function does not return 200 for OPTIONS, the actual request never fires. Fix: 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.
- **Querying rate_limit_log without an index on requested_at** — The rate limit check does a time-range query on every incoming request. Without an index, this full table scan gets slower as the log grows. Fix: 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.

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

---

Source: https://www.rapidevelopers.com/how-to-build-lovable/api-backend
© RapidDev — https://www.rapidevelopers.com/how-to-build-lovable/api-backend
