Back to Case Studies
Aurora2026/Marketing

Aurora CMS: Practical Defense-in-Depth Form Protection with Next.js and Supabase

How Aurora CMS eliminated form spam without reCAPTCHA using a five-stage defense pipeline built from server-side token generation, honeypots, origin validation, memory-safe rate limiting, and audit logging in Next.js and Supabase. Zero third-party dependencies, zero false positives after tuning.

Impact Result

Spam dropped to near-zero within days. Zero legitimate submissions blocked after threshold tuning. Submission latency held under 300ms with no CAPTCHA, no vendor costs, and no JavaScript requirement.

Aurora CMS: Practical Defense-in-Depth Form Protection with Next.js and Supabase

Contact forms are a quiet liability on most creative agency websites. Aurora, a portfolio CMS for a visual manufacturing studio, needed robust spam protection without the privacy baggage, accessibility penalties, and vendor lock-in that come with Google reCAPTCHA and its alternatives.

The solution was a five-stage, defense-in-depth form submission pipeline built entirely from native web primitives, cryptographic tokens, and PostgreSQL. No third-party CAPTCHA. No per-request costs. No JavaScript requirement.

This case study details the architecture, the hardening decisions that separate it from a naive implementation, and the trade-offs involved.


The Challenge

Contact forms on creative agency websites are frequent targets for automated abuse:

  • Simple bots that blindly fill every field

  • Headless browser scripts submitting at scale

  • Rate abuse from rotating IPs or impatient legitimate users

  • CSRF-style submissions from unauthorized origins

Traditional solutions like reCAPTCHA introduce privacy concerns, accessibility issues, extra JavaScript payload, and ongoing vendor costs. For Aurora, the goal was to create a self-contained system that stops the majority of spam effectively, works even if JavaScript is disabled, provides full audit visibility for admins, and adds minimal friction for high-value project inquiries.


The Architecture: Five-Stage Defense Pipeline

The protection runs server-side using Next.js Route Handlers and Supabase for persistence and logging.

Stage 1: Visitor Attribution (Middleware)

export async function middleware(request: NextRequest) {
  const response = NextResponse.next();

  if (!request.cookies.has("aurora_visitor_id")) {
    const visitorId = crypto.randomUUID();
    response.cookies.set("aurora_visitor_id", visitorId, {
      path: "/",
      maxAge: 60 * 60 * 24 * 365,
      sameSite: "lax",
      httpOnly: false,
      secure: process.env.NODE_ENV === "production",
    });
  }

  return response;
}

This assigns a persistent, pseudonymized visitor ID for correlation and analytics without collecting personal data.

Stage 2: Server-Side Token Generation

Tokens are generated during Server Component render (no public /api/token endpoint):

// In contact/page.tsx (Server Component)
const csrfToken = await generateCsrfToken();

export async function generateCsrfToken(): Promise<string> {
  const supabase = await createClient();
  const token = crypto.randomBytes(32).toString("hex");
  const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes

  await supabase.from("form_tokens").insert({
    token,
    expires_at: expiresAt.toISOString(),
    used: false,
  });

  return token;
}

This approach binds token creation to legitimate page renders.

Token Issuance Comparison:

Traditional (Vulnerable)                  Aurora (Hardened)
    |                                          |
    | ──GET /api/token────>                  | Server Component Render
    | <────token───────────                  | (token generated server-side)
    |                                          |
    | ──POST /form (w/ token)                | ──POST /form (w/ token from HTML)
    |                                          |
[Attacker can hammer /token endlessly]      [No /token endpoint exists]

By eliminating the token endpoint, Aurora removes an entire class of resource exhaustion attacks. Token generation is now bound to legitimate page renders, which are inherently more expensive and harder to abuse at scale.

Stage 3: Sequential Validation Pipeline

The submission handler (/api/actions/form/route.ts) applies checks in order:

Honeypot Check

if (website && website.trim() !== '') {
  await logSubmission({ blocked: true, blockReason: 'honeypot', ... });
  return NextResponse.json({ message: 'Inquiry sent successfully!' }, { status: 200 });
}

A hidden website field (styled with Tailwind display: none) catches many simple bots. Fake success response wastes attacker time.

Origin & Referer Validation
Strict allow-list check on Origin or Referer headers to prevent CSRF and cross-site submissions.

Token Validation

  • Token must exist, not be used, and not expired

  • Marked used: true atomically on success

Rate Limiting (Memory-Safe)

const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
const { count } = await supabase
  .from("form_submissions")
  .select("*", { count: "exact", head: true })
  .eq("ip_address", ip)
  .gte("submitted_at", tenMinutesAgo.toISOString());

if (count >= 5) {
  await logSubmission({ blocked: true, blockReason: "rate_limit" });
  return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}

Under a volumetric attack of 10,000 requests in 10 minutes:

  • Naive approach (.select('*')): Each query could return thousands of rows with ~1KB JSON form_data each → up to 50 GB of memory pressure across requests, easily causing out-of-memory crashes.

  • Optimized approach (count-only query): Returns a single integer → roughly 160 KB total memory across all requests.

The database handles aggregation efficiently, keeping application memory usage near-constant.

IP Extraction & Trust Boundary

IP extraction trusts forwarded headers only because Aurora runs behind Vercel’s Edge Network:

┌─────────────────────────────────────────────────────────────┐
│                      INTERNET                               │
│  Attacker ──> [X-Forwarded-For: forged IP]                  │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              VERCEL EDGE NETWORK (Trusted Proxy)            │
│  • Strips all client-supplied X-Forwarded-For               │
│  • Observes actual TCP source IP                            │
│  • Sets X-Forwarded-For: <real client IP>                   │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              AURORA APPLICATION                             │
│  • Safely trusts X-Forwarded-For (sanitized by edge)       │
│  • Extracts first IP from chain                             │
│  • Applies rate limiting                                    │
└─────────────────────────────────────────────────────────────┘

Self-hosted deployments must replicate this trust boundary using tools like Nginx set_real_ip_from, Traefik trusted IPs, or Cloudflare CF-Connecting-IP.

Stage 4: Comprehensive Audit Logging

Every attempt — blocked or successful — is logged with full context:

async function logSubmission(data: LogData) {
  await supabase.from("form_submissions").insert({
    ip_address: data.ip,
    user_agent: data.userAgent,
    success: data.success,
    blocked: data.blocked,
    block_reason: data.blockReason,
    form_data: data.formData ? sanitizeFormData(data.formData) : null,
    visitor_id: data.visitorId,
  });
}

The form_submissions table serves as both a security log and a debugging tool. In practice, the audit trail revealed patterns that shaped threshold tuning: the majority of blocked submissions hit the honeypot stage, a smaller share failed token validation (typically headless browsers that rendered the page but submitted after token expiry), and rate limiting caught the remainder. False positives - legitimate users blocked by rate limiting - were identified by cross-referencing visitor IDs against success: false entries and adjusting the 10-minute window accordingly.

Without this log, threshold tuning would be guesswork.


Key Database Schema

CREATE TABLE form_tokens (
  id SERIAL PRIMARY KEY,
  token TEXT UNIQUE NOT NULL,
  used BOOLEAN DEFAULT FALSE,
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE TABLE form_submissions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ip_address TEXT,
  user_agent TEXT,
  success BOOLEAN DEFAULT FALSE,
  blocked BOOLEAN DEFAULT FALSE,
  block_reason TEXT,
  form_data JSONB,
  visitor_id UUID REFERENCES visitors(id) ON DELETE SET NULL,
  submitted_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_form_submissions_ip_date
ON form_submissions(ip_address, submitted_at);

Observed Results

Stage

Block Mechanism

Observed Contribution

Honeypot

Hidden field detection

Majority of automated blocks

Token validation

Expired / missing token

Headless browsers, scrapers

Rate limiting

IP-based 10-min window

Repeat submitters, burst bots

Origin validation

Allow-list header check

Cross-site and CSRF attempts

Legitimate users

Zero false positives observed after threshold tuning

Key outcomes:

  • Spam submissions dropped to near-zero within days of deployment

  • No legitimate submissions blocked after the rate limit window was calibrated

  • Submission latency for real users remained under 300ms end-to-end

  • Zero third-party dependencies, zero per-request CAPTCHA costs

  • Full audit visibility enabled retroactive attack pattern analysis

Architectural Trade-offs

  • Honeypot limitations: Simple hidden fields catch many basic bots but can be bypassed by advanced headless browsers...

  • Token generation cost: Generating a token on every contact page render adds a small database write...

  • Rate limiting scope: IP-based limiting works well behind a trusted proxy but can be less effective against distributed attacks...

  • Maintenance overhead: The audit table grows over time and requires periodic archiving or retention policies...

  • Threat model calibration: This system is tuned for typical agency spam, not nation-state actors or highly determined attackers...

For Aurora's actual traffic and threat profile, the system has proven reliable and low-maintenance.


Conclusion

Aurora's form protection demonstrates that strong spam defense doesn't require heavy third-party services or degraded user experience. By carefully composing middleware, server-side token generation, honeypots, origin checks, memory-safe rate limiting, and audit logging, the team created a practical, auditable security boundary using only standard tools.

The three hardening decisions that matter most: eliminate the token endpoint entirely, use count-only queries under rate limiting load, and define your IP trust boundary explicitly before you need it. Everything else is implementation detail.

Most B2B creative sites don't need reCAPTCHA. They need a well-understood threat model and a layered system that's honest about what it stops. This is that system.

Interested in similar results?

Let's talk about your project