Security
5 Nov 2026
12 min read

Rate Limiting and Security: Protecting Your Micro-App from Abuse

Implementing rate limiting, SSRF protection, input validation, and other security best practices in SpaceZero edge functions.

Rate Limiting and Security

SpaceZero includes multiple layers of security for edge functions.

Rate Limiting

In-Memory Fixed Window

typescript

// functions/lib/rateLimit.ts

interface RateLimitOptions {

maxRequests?: number;

windowMs?: number;

}

 

// Simple in-memory store — per-isolate, resets on cold start.

// For distributed persistence, swap in Redis or D1.

const store = new Map<string, { count: number; resetAt: number }>();

const DEFAULT_MAX_REQUESTS = 60;

const DEFAULT_WINDOW_MS = 60_000;

 

function getKey(context: { request: Request }): string {

const ip =

context.request.headers.get('CF-Connecting-IP') ||

context.request.headers.get('X-Forwarded-For') ||

'unknown';

return ip;

}

Applied to Auth Endpoints

typescript

// functions/api/auth/check-user.ts — 10 requests / 60s

const limitResult = rateLimit({ request }, { maxRequests: 10, windowMs: 60_000 });

if (limitResult.limited) return limitResult.response!;

 

// functions/api/auth/send-email.ts — 5 requests / 60s

const limitResult = rateLimit(context, { maxRequests: 5, windowMs: 60_000 });

Newsletter Endpoints

typescript

// functions/api/newsletter.ts — POST subscribe: 10 / 60s, GET unsubscribe: 20 / 60s

const limitResult = rateLimit(context, { maxRequests: 10, windowMs: 60_000 });

SSRF Protection

URL Validation

typescript

// functions/lib/security.ts

export function validateUrlForSSRFSafety(targetUrl: string): {

valid: boolean;

reason?: string;

sanitizedUrl?: string;

}

 

// Blocks: non-http(s) protocols, embedded credentials,

// localhost/.local/.internal hostnames, and private/loopback/

// link-local/metadata IPs via isBlockedIPv4() and isBlockedIPv6().

Usage

typescript

const { valid, reason } = validateUrlForSSRFSafety(body.url);

if (!valid) {

return createErrorResponse(400, reason ?? 'URL not allowed');

}

Input Validation

JSON Body Validation

typescript

// functions/lib/validation.ts

const { data: body, error } = await validateJsonBody<{ email: string }>(request, {

email: { type: 'string', required: true },

});

if (error || !body) return createErrorResponse(400, 'Invalid request body');

Email Validation

typescript

export function validateEmail(email: string): boolean {

const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

return pattern.test(email);

}

Error Responses

typescript

export function createErrorResponse(status: number, message: string, details?: unknown) {

return new Response(JSON.stringify({ error: message, details }), {

status,

headers: { 'Content-Type': 'application/json' },

});

}

Gumroad Webhook Security

Shared-Secret Verification

typescript

const secret =

request.headers.get('x-gumroad-signature') ||

(body.custom_fields?.['secret'] as string) ||

(body.secret as string);

 

if (secret !== env.GUMROAD_WEBHOOK_SECRET) {

return createErrorResponse(401, 'Unauthorized signature');

}

CORS

SpaceZero functions are same-origin — the SPA and the API share one Cloudflare Pages deployment, so no CORS layer is included. If you split the API onto its own origin, add per-route Access-Control-Allow-* headers (and handle OPTIONS preflight) yourself.

Security Checklist

  • [ ] Rate limiting on all public endpoints
  • [ ] SSRF protection on outbound requests
  • [ ] Input validation on all inputs
  • [ ] Shared-secret verification on webhooks
  • [ ] Secure headers (CSP, HSTS, etc.)
  • [ ] Secrets in Cloudflare Pages secrets (not .env)
  • [ ] Service role key only in server functions
  • [ ] RLS enabled on all tables