Captcha (Bot Protection)
EdgeBase uses Cloudflare Turnstile to automatically protect authentication endpoints from bots. With zero configuration required for most setups.
Quick Start
Add one line to your config:
// edgebase.config.ts
export default defineConfig({
captcha: true,
});
That's it. When you run npx edgebase deploy, Turnstile keys are automatically provisioned, stored, and distributed to your SDKs.
How It Works
Client SDK Server Cloudflare
│ │ │
├─ GET /api/config ────────►│ │
│◄─── { captcha: { siteKey } } │
│ │ │
├─ Turnstile widget ───────────────────────────────────►│
│◄── token (invisible, ~200ms) ◄────────────────────────│
│ │ │
├─ POST /auth/signup ──────►│ │
│ { captchaToken: "..." } ├─ siteverify(token) ──────►│
│ │◄── { success: true } ◄────│
│◄── 201 Created ◄─────────│ │
Phase 1: Invisible (99% of users)
Turnstile runs invisibly in the background. Users see nothing — the token is acquired automatically in ~200ms.
Phase 2: Interactive Challenge (1% of users)
If Cloudflare detects suspicious behavior, an interactive challenge (checkbox or puzzle) is shown as an overlay. The SDK handles this automatically.
Configuration
Zero-Config (Cloudflare Deploy)
captcha: true
npx edgebase deployauto-provisions Turnstile widget via Cloudflare Management APIsiteKeyis exposed to clients viaCAPTCHA_SITE_KEYandGET /api/configsecretKeyis stored as Workers SecretTURNSTILE_SECRET
Manual Keys (Self-Hosting / Docker)
captcha: {
siteKey: '0x4AAAAAAA...', // From Turnstile dashboard
secretKey: '0x4AAAAAAA...',
failMode: 'open', // 'open' (default) | 'closed'
siteverifyTimeout: 3000, // ms (default: 3000)
}
Disable Captcha
captcha: false // or omit entirely
SDK Usage
Zero-Config (Recommended)
Client SDKs handle captcha automatically once the runtime host is in place. No per-request code changes are needed:
- JavaScript
- Dart/Flutter
- Swift
- Kotlin
- Java
- C#
- C++
const client = createClient('https://your-project.edgebase.dev');
await client.auth.signUp({ email: 'user@test.com', password: 'pass123' });
// SDK automatically: fetches siteKey → runs Turnstile → attaches token
final client = ClientEdgeBase('...');
await client.auth.signUp(SignUpOptions(email: 'user@test.com', password: 'pass123'));
let client = EdgeBaseClient("...")
try await client.auth.signUp(email: "user@test.com", password: "pass123")
val client = ClientEdgeBase("...")
client.auth.signUp(email = "user@test.com", password = "pass123")
ClientEdgeBase client = EdgeBase.client("...");
client.auth().signUp("user@test.com", "pass123");
var client = new EdgeBase("...");
await client.Auth.SignUpAsync("user@test.com", "pass123");
eb::EdgeBase client("...");
client.auth().signUp("user@test.com", "pass123");
Client Runtime Support Matrix
If you are comparing package/module layout instead of runtime support, see SDK Layer Matrix.
Legend:
✅: supported and validated in a current example/runtime flow◐: supported by the SDK or target host, but still depends on a specific host/plugin or has not been fully re-validated in every runtime—: no client-side captcha path
| SDK | Android | iOS | macOS | Windows | Linux | Web |
|---|---|---|---|---|---|---|
@edgebase/web | — | — | ✅ | ◐ | ◐ | ✅ |
@edgebase/react-native | ✅ | ✅ | — | — | — | ✅ |
edgebase_flutter | ✅ | ✅ | ✅ | ◐ | ◐ | ✅ |
EdgeBase Swift | — | ✅ | ✅ | — | — | — |
Kotlin KMP client | ✅ | ✅ | ✅ | — | — | ✅ |
Android Java | ✅ | — | — | — | — | — |
Unity C# | ✅ | ✅ | ✅ | ◐ | ◐ | ✅ |
Unreal / C++ | ✅ | ✅ | ✅ | ◐ | ◐ | — |
Notes:
@edgebase/webdesktop columns mean browser-hosted runtimes such as Electron renderer processes, not a native desktop-only SDK.Kotlin KMP clientuses a no-op JVM captcha provider, so Windows/Linux desktop JVM targets are intentionally not marked supported here.Unity C#desktop support depends on a supported WebView host. The current macOS path is validated through an embeddedgree/unity-webviewwindow. Other desktop targets still require a supported host integration or a customTurnstileProvider.SetWebViewFactory(...).Unreal / C++uses the built-in browser runtime on supported targets; macOS, Android, and iOS are validated in the current example app flow.
Manual Token Override
If you need custom captcha UI or use a different captcha provider:
await client.auth.signUp({
email: 'user@test.com',
password: 'pass123',
captchaToken: myCustomToken, // Skips built-in Turnstile
});
Protected Endpoints
| Endpoint | Action | Captcha |
|---|---|---|
POST /auth/signup | signup | ✅ |
POST /auth/signin | signin | ✅ |
POST /auth/signin/anonymous | anonymous | ✅ |
POST /auth/request-password-reset | password-reset | ✅ |
GET /auth/oauth/:provider | oauth | ✅ |
POST /auth/refresh | — | ❌ (session renewal) |
POST /auth/signout | — | ❌ (logout) |
POST /auth/change-password | — | ❌ (authenticated) |
Functions with Captcha
You can enable captcha on individual HTTP functions:
// functions/submit-form.ts
export default defineFunction({
trigger: { type: 'http' },
captcha: true, // Requires captcha token
handler: async (context) => { ... },
});
Platform-Specific Details
Web (JS/TS, Flutter Web, Kotlin JS)
Turnstile JS SDK is loaded directly into the browser DOM. No WebView needed.
Android (Kotlin, Java)
- Uses
android.webkit.WebView(system built-in, zero dependencies) - Zero-config: Automatically detects
Applicationcontext viaActivityThread.currentApplication()reflection - Automatically tracks current foreground
ActivityviaActivityLifecycleCallbacks - Interactive challenges shown as dimmed overlay on current Activity
iOS/macOS (Swift, Kotlin Apple)
- Uses
WKWebView(system built-in, zero dependencies) - Interactive challenges shown as overlay on key window
- iOS:
UIWindowScene→keyWindowoverlay - macOS:
NSApplication.keyWindowoverlay
Unity (C#)
Built-in adapters for popular WebView plugins (auto-detected at startup):
| Plugin | Define Symbol | License |
|---|---|---|
| UniWebView | UNIWEBVIEW | Paid |
| Vuplex 3D WebView | VUPLEX_WEBVIEW | Paid |
| gree/unity-webview | UNITY_WEBVIEW_GREE | Free (MIT) |
Add the define symbol in Player Settings → Scripting Define Symbols. The adapter auto-registers at startup.
If no supported plugin is detected, a warning is logged. You can provide a custom factory:
TurnstileProvider.SetWebViewFactory(async (siteKey, action) => {
var html = TurnstileProvider.GetTurnstileHtml(siteKey, action);
// Load html in your WebView, capture token from JS bridge
return token;
});
Unreal Engine (C++)
Uses the built-in SWebBrowser widget (CEF-based). Zero third-party dependencies.
Setup: Add to your .Build.cs:
PublicDependencyModuleNames.AddRange(new string[] {
"WebBrowserWidget", "Slate", "SlateCore"
});
Then include the header in any .cpp file:
#include "edgebase/turnstile_adapter_ue.h"
The adapter auto-registers via FCoreDelegates::OnPostEngineInit. No manual calls needed.
Flutter (Dart)
Uses flutter_inappwebview for all native platforms (Android, iOS, macOS, Windows, Linux). Already included as a dependency in the SDK.
# Already in edgebase SDK's pubspec.yaml
dependencies:
flutter_inappwebview: ^6.0.0
No additional setup needed. Platform detection is automatic via conditional imports.
Security
| Aspect | Detail |
|---|---|
| siteKey | Public — safely distributed via GET /api/config |
| secretKey | Private — stored as Workers Secret TURNSTILE_SECRET, never exposed |
| Token one-use | Cloudflare auto-invalidates tokens after siteverify |
| Action verification | Prevents token reuse across endpoints (signup token can't be used for signin) |
| Service Key bypass | Server SDKs (Go, PHP, Rust, Python) bypass captcha when using Service Keys |
| CDN caching | /api/config cached for 60s at edge (reduces Worker invocations) |
Fail Modes
| Mode | Behavior on Turnstile API Failure | Use Case |
|---|---|---|
open (default) | Allow request through, log warning | General apps (availability first) |
closed | Reject with 503 | Finance, healthcare (security first) |
Rate Limiting + Captcha
Captcha and rate limiting work together as complementary layers:
Request → CORS → Rate Limit → Captcha → Auth Handler
- Rate limiting runs first — blocks brute-force regardless of captcha status
- Captcha runs second — verifies human origin for requests that pass rate limit
- Both layers are independent — disabling one doesn't affect the other
- Service Keys bypass captcha and EdgeBase app-level rate limits
Testing
Disable in Tests (Recommended)
# wrangler.test.toml — omit captcha config / CAPTCHA_SITE_KEY
Cloudflare Test Keys
| Purpose | siteKey | secretKey |
|---|---|---|
| Always passes | 1x00000000000000000000AA | 1x0000000000000000000000000000000AA |
| Always fails | 2x00000000000000000000AB | 2x0000000000000000000000000000000AB |
| Forces interactive | 3x00000000000000000000FF | 3x0000000000000000000000000000000FF |
// Use in test config for E2E captcha testing
captcha: {
siteKey: '1x00000000000000000000AA',
secretKey: '1x0000000000000000000000000000000AA',
}
Troubleshooting
Captcha not working in development
captcha: true requires Cloudflare deploy for auto-provisioning. For local development, either:
- Use manual keys:
captcha: { siteKey: '...', secretKey: '...' } - Disable captcha:
captcha: false
Interactive challenge keeps appearing
This usually means Cloudflare's bot detection is triggered. Common causes:
- Running from a datacenter IP
- Automated testing without test keys
- VPN or proxy usage
SDK not automatically acquiring tokens
Verify GET /api/config returns { captcha: { siteKey: "..." } }. If captcha is null, captcha is not configured on the server.