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
- Log into the Cloudflare Dashboard
- Navigate to Turnstile in the sidebar
- Click Add Widget
- Enter a widget name (e.g., "Supabase Auth")
- Add your domain(s) to the allowed list
- Choose Managed mode for invisible challenges (recommended)
- 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
- Create an account at hCaptcha Dashboard
- Add a new site with your domain
- 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 },
})
Magic Links
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:
- Domain mismatch: Your site key must be registered for the domain you're testing from
- CSP headers: Content Security Policy may block the CAPTCHA script. Add the provider's domains to your policy
- 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:
- Verify your secret key is correctly set in production environment variables
- Check that the auth container has network access to the CAPTCHA provider's API
- Ensure
GOTRUE_SECURITY_CAPTCHA_TIMEOUTallows 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.
