Starter package only — this repository is a minimal template to bootstrap your own API. Clone it, configure Supabase and Vercel, then extend routes, auth, and schema for your product. It is not a production-ready application as-is.
Basic Express.js API deployed on Vercel only, with Supabase, optional JWT auth, Zod validation, rate limiting, and axios for external HTTP.
- Clone the repo and install dependencies:
npm install- Copy environment variables:
cp .env.example .env-
Fill in Supabase credentials from Project Settings → API:
SUPABASE_URLSUPABASE_ANON_KEY(publishable / anon key)SUPABASE_SERVICE_ROLE_KEY(server only — never expose to clients)
-
Create the database table and RLS policies. Run
supabase/schema.sqlin the Supabase SQL Editor. -
Local development (recommended — matches Vercel serverless):
vercel link
vercel env pull
npm run devOptional fallback without Vercel CLI (may differ slightly from production):
npm run dev:local| Variable | Required | Description |
|---|---|---|
SUPABASE_URL |
Yes | Supabase project URL |
SUPABASE_ANON_KEY |
Yes | Anon key for JWT validation and RLS-scoped queries |
SUPABASE_SERVICE_ROLE_KEY |
Yes | Service role (server only; reserved for admin tasks) |
RATE_LIMIT_WINDOW_MS |
No | Rate limit window (default 900000 = 15 min) |
RATE_LIMIT_MAX |
No | Max requests per window per IP (default 100) |
PORT |
No | Port for dev:local only (default 3000) |
Set the same variables in the Vercel dashboard for Production and Preview, or use vercel env pull.
Endpoints under /api/* accept an optional Bearer token. Auth is never required unless you use ?mine=true on GET /api/items.
| Scenario | Behavior |
|---|---|
No Authorization header |
Anonymous request; req.user is unset |
Valid Authorization: Bearer <token> |
User attached; Supabase client uses caller JWT (RLS applies) |
| Invalid or expired token | 401 Unauthorized |
- Dashboard: Auth → Users → create a user, then use the Supabase client or Auth API to sign in.
- Client SDK:
supabase.auth.signInWithPassword({ email, password })and readsession.access_token.
Example:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" https://YOUR_DEPLOYMENT.vercel.app/api/meGlobal limit: 100 requests / 15 minutes / IP (configurable via env).
On Vercel, limits apply per serverless instance, not globally across all regions. For strict distributed limits in production, consider Upstash Ratelimit.
- Push the repository to GitHub.
- Import the project in Vercel (framework preset: Other).
- Set environment variables in the Vercel dashboard.
- Deploy.
vercel.json rewrites all routes to api/index.js, which exports the Express app.
Replace BASE with your deployment URL (e.g. https://api-project.vercel.app).
# Health (no Supabase auth needed for route; env must still be set on server)
curl "$BASE/health"
# Auth status (anonymous)
curl "$BASE/api/me"
# List items
curl "$BASE/api/items"
# Create item (anonymous)
curl -X POST "$BASE/api/items" \
-H "Content-Type: application/json" \
-d '{"name":"Test item"}'
# With authentication
curl "$BASE/api/me" -H "Authorization: Bearer YOUR_TOKEN"
curl "$BASE/api/items?mine=true" -H "Authorization: Bearer YOUR_TOKEN"
curl -X POST "$BASE/api/items" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"name":"My item"}'
# External proxy
curl "$BASE/api/external"Base URL: your Vercel deployment or http://localhost:3000 when using dev:local.
Errors use: { "error": "message", "details": ... } (details optional).
Liveness check. No authentication.
Response 200
{ "status": "ok" }curl http://localhost:3000/healthReturns whether the caller is authenticated.
Headers (optional): Authorization: Bearer <supabase_access_token>
Response 200 (anonymous)
{ "authenticated": false }Response 200 (authenticated)
{
"authenticated": true,
"user": { "id": "uuid", "email": "user@example.com" }
}Response 401 — invalid token when Authorization header is sent
{ "error": "Invalid or expired token" }List items from Supabase.
Headers (optional): Authorization: Bearer <token>
Query parameters
| Param | Type | Description |
|---|---|---|
mine |
true |
When true, return only items owned by the authenticated user. Requires valid Bearer token. |
Response 200
{
"items": [
{
"id": "uuid",
"name": "Example",
"user_id": null,
"created_at": "2026-01-01T00:00:00.000Z"
}
]
}Response 401 — ?mine=true without valid token
curl http://localhost:3000/api/items
curl "http://localhost:3000/api/items?mine=true" -H "Authorization: Bearer TOKEN"Create an item.
Headers: Content-Type: application/json
Headers (optional): Authorization: Bearer <token>
Body
{ "name": "Item name" }| Field | Type | Rules |
|---|---|---|
name |
string | Required, 1–200 characters |
When authenticated, user_id is set to the caller. When anonymous, user_id is null.
Response 201
{
"item": {
"id": "uuid",
"name": "Item name",
"user_id": "uuid-or-null",
"created_at": "2026-01-01T00:00:00.000Z"
}
}Response 400 — validation error
curl -X POST http://localhost:3000/api/items \
-H "Content-Type: application/json" \
-d '{"name":"Hello"}'Fetches a random quote from Quotable via axios (5s timeout).
Headers (optional): Authorization: Bearer <token> (auth middleware runs; behavior is unchanged)
Response 200
{
"source": "quotable",
"data": { "_id": "...", "content": "...", "author": "..." }
}Response 502 / 504 — upstream failure or timeout
curl http://localhost:3000/api/external| Status | When |
|---|---|
400 |
Request validation failed (Zod) |
401 |
Invalid Bearer token, or GET /api/items?mine=true without authentication |
404 |
Unknown route |
429 |
Rate limit exceeded |
500 |
Server or Supabase error |
502 / 504 |
Upstream failure on GET /api/external |
- Never commit
.envor exposeSUPABASE_SERVICE_ROLE_KEYto clients. - The anon key is used server-side for JWT validation and RLS-scoped queries.
- Enable RLS on all public tables (see
supabase/schema.sql).
api/ Vercel serverless entry
src/ Express app, routes, middleware
supabase/ SQL schema for items table + RLS
vercel.json Vercel rewrites and function config