CAPTCHA Protection for Self-Hosted Supabase: Turnstile and hCaptcha Setup

Configure CAPTCHA protection for self-hosted Supabase using Cloudflare Turnstile or hCaptcha to prevent bot attacks and abuse.

Cover Image for CAPTCHA Protection for Self-Hosted Supabase: Turnstile and hCaptcha Setup

Bots and automated attacks are a constant threat to authentication systems. Credential stuffing, account enumeration, and signup spam can overwhelm your self-hosted Supabase instance—burning through resources and compromising user accounts. CAPTCHA protection adds a critical layer of defense that separates legitimate users from automated attackers.

Supabase Cloud offers CAPTCHA through a simple dashboard toggle. On self-hosted Supabase, you'll configure it through environment variables in your docker-compose.yml. This guide covers setting up both Cloudflare Turnstile (the modern, friction-free choice) and hCaptcha (the privacy-focused alternative), along with frontend integration patterns.

Why CAPTCHA Matters for Self-Hosted Deployments

Self-hosted Supabase instances lack some of the built-in protections that Supabase Cloud provides. While you can implement rate limiting at the reverse proxy level, rate limits alone don't stop sophisticated attacks that distribute requests across multiple IPs.

CAPTCHA provides several benefits:

  • Prevents credential stuffing: Attackers can't automate password attempts against your login endpoint
  • Stops signup spam: Bots can't create thousands of fake accounts to abuse your service
  • Protects password reset: Prevents attackers from enumerating valid email addresses
  • Reduces resource consumption: Each failed CAPTCHA challenge saves database queries and compute

Modern CAPTCHA solutions like Cloudflare Turnstile operate invisibly 99% of the time. They analyze browser signals and behavior patterns, only presenting a challenge when something looks suspicious. This "frictionless security" approach has made CAPTCHA protection standard for production authentication flows.

Choosing Between Turnstile and hCaptcha

Supabase supports two CAPTCHA providers, each with distinct tradeoffs:

Cloudflare Turnstile

Turnstile is Cloudflare's CAPTCHA replacement that prioritizes user experience. Most users never see a challenge—the system verifies humanity through passive signals like browser fingerprints and interaction patterns.

Pros:

  • Free for unlimited verifications
  • Invisible verification for most users
  • No accessibility concerns with image challenges
  • Fast integration with Cloudflare ecosystem

Cons:

  • Requires trusting Cloudflare's infrastructure
  • Less battle-tested than older solutions

hCaptcha

hCaptcha is a privacy-focused alternative that compensates website owners for verified challenges. It uses traditional image-based challenges when needed.

Pros:

  • GDPR-compliant by design
  • Revenue sharing model available
  • Enterprise-grade reliability
  • Works without JavaScript in degraded mode

Cons:

  • Image challenges create friction
  • Accessibility challenges for visually impaired users
  • Free tier has some limitations

For most self-hosted deployments, Turnstile is the recommended choice. Its invisible verification model converts better and requires less user effort. Choose hCaptcha if you need strict GDPR compliance or prefer to avoid Cloudflare's ecosystem entirely.

Getting Your CAPTCHA Credentials

Before configuring Supabase, you need to obtain API keys from your chosen provider.

Cloudflare Turnstile Setup

  1. Log into the Cloudflare Dashboard
  2. Navigate to Turnstile in the sidebar
  3. Click Add Widget
  4. Enter a widget name (e.g., "Supabase Auth")
  5. Add your domain(s) to the allowed list
  6. Choose Managed mode for invisible challenges (recommended)
  7. Copy your Site Key (public) and Secret Key (private)

The Site Key goes in your frontend code. The Secret Key goes in your Supabase environment variables—never expose it publicly.

hCaptcha Setup

  1. Create an account at hCaptcha Dashboard
  2. Add a new site with your domain
  3. Copy your Site Key and Secret Key

For local development, hCaptcha requires either an ngrok tunnel or a hosts file entry pointing your domain to localhost. The service won't verify challenges from localhost or 127.0.0.1 directly.

Configuring Self-Hosted Supabase for CAPTCHA

CAPTCHA configuration happens in your Docker environment. Unlike Supabase Cloud, there's no dashboard toggle—you'll set environment variables directly.

Environment Variables

Add these variables to your .env file:

# Enable CAPTCHA protection
SECURITY_CAPTCHA_ENABLED=true

# Choose provider: "turnstile" or "hcaptcha"
SECURITY_CAPTCHA_PROVIDER=turnstile

# Your secret key from the provider
SECURITY_CAPTCHA_SECRET=0x4AAAAAAA...your_secret_key

# Timeout for CAPTCHA verification (optional)
SECURITY_CAPTCHA_TIMEOUT=10s

Docker Compose Configuration

Update your docker-compose.yml to pass these variables to the auth service:

services:
  auth:
    image: supabase/gotrue:v2.167.0
    environment:
      GOTRUE_SECURITY_CAPTCHA_ENABLED: ${SECURITY_CAPTCHA_ENABLED:-false}
      GOTRUE_SECURITY_CAPTCHA_PROVIDER: ${SECURITY_CAPTCHA_PROVIDER:-turnstile}
      GOTRUE_SECURITY_CAPTCHA_SECRET: ${SECURITY_CAPTCHA_SECRET}
      GOTRUE_SECURITY_CAPTCHA_TIMEOUT: ${SECURITY_CAPTCHA_TIMEOUT:-10s}
      # ... other auth environment variables

After updating your configuration, restart the auth container:

docker compose down auth && docker compose up -d auth

Verify the configuration by checking the container logs:

docker compose logs auth | grep -i captcha

You should see a message indicating CAPTCHA protection is enabled.

Frontend Integration with React

Once the backend is configured, your frontend must include the CAPTCHA widget and pass verification tokens with authentication requests.

Turnstile with React

Install the official Turnstile React component:

npm install @marsidev/react-turnstile

Create a login form that captures the Turnstile token:

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [captchaToken, setCaptchaToken] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    
    if (!captchaToken) {
      setError('Please complete the CAPTCHA verification')
      return
    }

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
      options: {
        captchaToken,
      },
    })

    if (error) {
      setError(error.message)
    }
  }

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        required
      />
      
      <Turnstile
        siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
        onSuccess={setCaptchaToken}
        onError={() => setError('CAPTCHA verification failed')}
        onExpire={() => setCaptchaToken(null)}
      />
      
      {error && <p className="error">{error}</p>}
      
      <button type="submit" disabled={!captchaToken}>
        Sign In
      </button>
    </form>
  )
}

hCaptcha with React

For hCaptcha, install the official React component:

npm install @hcaptcha/react-hcaptcha

The integration pattern is similar:

import HCaptcha from '@hcaptcha/react-hcaptcha'
import { useRef, useState } from 'react'

export function SignupForm() {
  const [captchaToken, setCaptchaToken] = useState<string | null>(null)
  const captchaRef = useRef<HCaptcha>(null)

  const handleSignup = async (e: React.FormEvent) => {
    e.preventDefault()
    
    if (!captchaToken) {
      // Trigger the CAPTCHA challenge
      captchaRef.current?.execute()
      return
    }

    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        captchaToken,
      },
    })

    // Reset CAPTCHA after submission
    captchaRef.current?.resetCaptcha()
    setCaptchaToken(null)
  }

  return (
    <form onSubmit={handleSignup}>
      {/* Form fields */}
      
      <HCaptcha
        ref={captchaRef}
        sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
        onVerify={setCaptchaToken}
        onExpire={() => setCaptchaToken(null)}
      />
      
      <button type="submit">Create Account</button>
    </form>
  )
}

Protecting All Auth Endpoints

CAPTCHA should protect every authentication endpoint that's vulnerable to automation:

Sign Up

The most critical endpoint to protect. Bot-created accounts lead to spam, abuse of free tiers, and potential reputation damage if used for malicious purposes.

await supabase.auth.signUp({
  email,
  password,
  options: { captchaToken },
})

Sign In

Credential stuffing attacks target login endpoints with leaked password databases. CAPTCHA makes these attacks economically unviable.

await supabase.auth.signInWithPassword({
  email,
  password,
  options: { captchaToken },
})

Password Reset

Attackers use password reset to enumerate valid email addresses. CAPTCHA prevents automated enumeration attempts.

await supabase.auth.resetPasswordForEmail(email, {
  captchaToken,
})

Anonymous Sign-Ins

If you've enabled anonymous authentication, protect it with CAPTCHA to prevent abuse:

await supabase.auth.signInAnonymously({
  options: { captchaToken },
})

One-time password and magic link flows should also require CAPTCHA to prevent email bombing:

await supabase.auth.signInWithOtp({
  email,
  options: { captchaToken },
})

Handling CAPTCHA Errors

When CAPTCHA verification fails, Supabase returns specific error codes. Handle these gracefully in your frontend:

const { error } = await supabase.auth.signInWithPassword({
  email,
  password,
  options: { captchaToken },
})

if (error) {
  switch (error.message) {
    case 'captcha verification process failed':
      // The token was invalid or expired
      setError('Security verification failed. Please try again.')
      captchaRef.current?.resetCaptcha()
      break
    case 'captcha_token is required':
      // CAPTCHA is enabled but no token was provided
      setError('Please complete the security check.')
      break
    default:
      setError(error.message)
  }
}

Testing CAPTCHA Locally

Testing CAPTCHA during development requires some additional setup since providers validate the requesting domain.

Turnstile Test Keys

Cloudflare provides test keys that always pass or fail for development:

Always passes:

  • Site Key: 1x00000000000000000000AA
  • Secret Key: 1x0000000000000000000000000000000AA

Always fails:

  • Site Key: 2x00000000000000000000AB
  • Secret Key: 2x0000000000000000000000000000000AB

Use these in your development .env:

# Development CAPTCHA (always passes)
SECURITY_CAPTCHA_SECRET=1x0000000000000000000000000000000AA

hCaptcha Test Keys

hCaptcha provides similar test credentials:

Always passes:

  • Site Key: 10000000-ffff-ffff-ffff-000000000001
  • Secret Key: 0x0000000000000000000000000000000000000000

Bypassing in E2E Tests

For automated testing, you can conditionally disable CAPTCHA in test environments:

# In your test environment .env
SECURITY_CAPTCHA_ENABLED=false

Alternatively, use test keys that always pass so your test suite exercises the actual CAPTCHA code paths.

Combining CAPTCHA with Other Security Measures

CAPTCHA is one layer in a defense-in-depth strategy. For comprehensive protection, combine it with:

Rate limiting: Configure your reverse proxy to limit requests per IP. CAPTCHA stops bots; rate limiting stops distributed attacks.

Strong password policies: Enable password strength requirements so even if an attacker bypasses CAPTCHA, weak passwords aren't a vulnerability.

MFA: For high-value accounts, require multi-factor authentication as a second layer after password verification.

Monitoring: Set up log management to detect unusual authentication patterns—many failed attempts from one IP, signups from disposable email domains, or other suspicious behavior.

Common Issues and Solutions

CAPTCHA Widget Not Loading

If the CAPTCHA widget doesn't appear, check:

  1. Domain mismatch: Your site key must be registered for the domain you're testing from
  2. CSP headers: Content Security Policy may block the CAPTCHA script. Add the provider's domains to your policy
  3. Ad blockers: Some browser extensions block CAPTCHA scripts

Token Expired Before Submission

CAPTCHA tokens have short lifespans (typically 2-5 minutes). If users take too long to complete forms:

<Turnstile
  onExpire={() => {
    setCaptchaToken(null)
    // Optionally auto-refresh
    turnstileRef.current?.reset()
  }}
/>

Verification Failing in Production

If tokens verify locally but fail in production:

  1. Verify your secret key is correctly set in production environment variables
  2. Check that the auth container has network access to the CAPTCHA provider's API
  3. Ensure GOTRUE_SECURITY_CAPTCHA_TIMEOUT allows enough time for network requests

Simplifying CAPTCHA Management

Configuring CAPTCHA through environment variables works, but managing these settings across multiple projects or environments adds operational complexity. Supascale provides a visual interface for configuring CAPTCHA protection alongside other auth settings—no manual environment variable editing required.

Whether you're managing one self-hosted instance or several, the Supascale dashboard lets you enable CAPTCHA, switch providers, and update secrets without restarting containers or editing compose files.

Conclusion

CAPTCHA protection is essential for production self-hosted Supabase deployments. Cloudflare Turnstile offers the best balance of security and user experience, while hCaptcha provides a privacy-focused alternative. Whichever you choose, the configuration follows the same pattern: set environment variables, restart the auth container, and integrate the widget into your frontend authentication flows.

Combined with rate limiting, strong password policies, and monitoring, CAPTCHA creates a robust defense against automated attacks—letting legitimate users sign in smoothly while keeping bots out.

Further Reading