Complete API reference for SpaceZero edge functions including authentication, payments, newsletter, and webhooks.
Complete reference for all SpaceZero edge functions.
Production: https://your-app.space/api/
Development: http://localhost:3000/api/
Auth is handled entirely client-side by the Supabase JS client (session persisted in localStorage). The edge functions are public endpoints — they do not authenticate users, they only validate input and rate-limit by IP.
No API keys needed for client calls.
| Endpoint | Limit | Window |
|---|---|---|
/api/auth/check-user | 10 req | 60s |
/api/auth/send-email | 5 req | 60s |
/api/newsletter (POST) | 10 req | 60s |
/api/newsletter/unsubscribe (GET) | 20 req | 60s |
/api/webhooks/gumroad | Unlimited | - |
Exceeded → 429 Too Many Requests
Health check for load balancers.
Response:
{
"status": "ok",
"service": "spacezero-api",
"framework": "cloudflare-pages-functions",
"version": "0.1.0",
"timestamp": "2026-12-10T00:00:00.000Z",
"bindings": {
"hasSupabaseServiceRole": true,
"hasGumroadSecret": true,
"appUrl": "https://your-app.space"
}
}
Check if email exists in Supabase Auth.
Request:
Response:
{
"exists": true
}
Errors:
400: Invalid email format429: Rate limitedSend transactional email via Resend.
Request:
{
"email": "[email protected]",
"type": "password_reset" | "email_confirmation",
"token": "reset-token",
"redirectUrl": "https://your-app.space/reset-password"
}
Response:
{
"sent": true,
"email": "[email protected]",
"type": "password_reset",
"note": "Only present when RESEND_API_KEY is not set — email is simulated."
}
Errors:
400: Invalid email or missing type429: Rate limited500: Resend API errorSign up with email/password (handled by Supabase client, not direct API).
Login with email/password (handled by Supabase client).
Logout (handled by Supabase client).
Subscribe email to ConvertKit.
Request:
Response:
States:
active - Confirmed subscriberunconfirmed - Pending double opt-incancelled - UnsubscribedErrors:
400: Invalid email409: Already subscribed429: Rate limited500: ConvertKit API errorUnsubscribe email from ConvertKit.
Query Parameters:
Response: HTML page confirming unsubscribe.
Errors:
400: Invalid email404: Email not found500: ConvertKit API errorGumroad purchase webhook handler.
Headers:
Content-Type: application/x-www-form-urlencoded
X-Gumroad-Signature: your-webhook-secret
Body (form data):
sale_id=12345&[email protected]&product_id=abc&refunded=false&custom_fields[user_id]=uuid
Response:
{
"received": true,
"sale_id": "12345",
"buyer_email": "[email protected]",
"product_id": "abc",
"user_id": "uuid",
"status": "active",
"db_status": "written",
"timestamp": "2026-12-10T00:00:00.000Z"
}
Verification:
X-Gumroad-Signature header, custom_fields.secret in the body, or a secret query parameter on the ping URLGUMROAD_WEBHOOK_SECRET is configuredErrors:
400: Missing sale_id or email (or unsupported Content-Type)401: Invalid signature500: Processing errorSpaceZero uses Supabase JavaScript client for all database/auth operations.
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
);
// Sign up
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'password123',
options: { emailRedirectTo: 'https://your-app.space/confirm' }
});
// Login
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password123',
});
// Logout
await supabase.auth.signOut();
// Password reset
await supabase.auth.resetPasswordForEmail('[email protected]', {
redirectTo: 'https://your-app.space/reset-password',
});
// Update password (after reset)
await supabase.auth.updateUser({ password: 'newpassword' });
// OAuth
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'https://your-app.space/' }
});
// Session
const { data: { session } } = await supabase.auth.getSession();
// User
const { data: { user } } = await supabase.auth.getUser();
// Query with RLS (user must be authenticated)
const { data, error } = await supabase
.from('user_profiles')
.select('*')
.eq('id', user.id)
.single();
// Upsert (RLS checks user_id)
const { data, error } = await supabase
.from('user_profiles')
.upsert({ id: user.id, full_name: 'New Name' });
// Update
const { data, error } = await supabase
.from('user_profiles')
.update({ full_name: 'New Name' })
.eq('id', user.id);
// Delete (RLS requires ownership)
const { data, error } = await supabase
.from('app_entitlements')
.delete()
.eq('gumroad_sale_id', saleId)
.eq('user_id', user.id);
// Upload (original file, original extension)
const fileId = `${user.id}/${Date.now()}.${file.name.split('.').pop()}`;
const { data, error } = await supabase.storage
.from('avatars')
.upload(fileId, file, { upsert: true });
// Public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(data.path);
// Delete
await supabase.storage.from('avatars').remove([path]);
const channel = supabase
.channel('entitlements')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'app_entitlements' }, payload => {
console.log('New entitlement:', payload.new);
})
.subscribe();
channel.unsubscribe();
All endpoints return consistent error format:
{
"error": "Human-readable error message",
"details": "Optional technical details"
}
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request (validation) |
| 401 | Unauthorized (signature) |
| 404 | Not Found |
| 409 | Conflict (already exists) |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 503 | Service Unavailable |
Gumroad retries failed webhooks with exponential backoff.
SpaceZero webhook is idempotent (safe to process multiple times).
Same-origin requests: No CORS headers needed.
Cross-origin: Not supported for security.
API version tied to app version in package.json.
Breaking changes only in major versions.
# Local testing with wrangler
wrangler dev
# Test webhook with ngrok
ngrok http 3000
# Configure: https://xxx.ngrok.io/api/webhooks/gumroad
console.log visible in dashboard