Skip to main content

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 deploy auto-provisions Turnstile widget via Cloudflare Management API
  • siteKey is exposed to clients via CAPTCHA_SITE_KEY and GET /api/config
  • secretKey is stored as Workers Secret TURNSTILE_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

Client SDKs handle captcha automatically once the runtime host is in place. No per-request code changes are needed:

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

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
SDKAndroidiOSmacOSWindowsLinuxWeb
@edgebase/web
@edgebase/react-native
edgebase_flutter
EdgeBase Swift
Kotlin KMP client
Android Java
Unity C#
Unreal / C++

Notes:

  • @edgebase/web desktop columns mean browser-hosted runtimes such as Electron renderer processes, not a native desktop-only SDK.
  • Kotlin KMP client uses 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 embedded gree/unity-webview window. Other desktop targets still require a supported host integration or a custom TurnstileProvider.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

EndpointActionCaptcha
POST /auth/signupsignup
POST /auth/signinsignin
POST /auth/signin/anonymousanonymous
POST /auth/request-password-resetpassword-reset
GET /auth/oauth/:provideroauth
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 Application context via ActivityThread.currentApplication() reflection
  • Automatically tracks current foreground Activity via ActivityLifecycleCallbacks
  • 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: UIWindowScenekeyWindow overlay
  • macOS: NSApplication.keyWindow overlay

Unity (C#)

Built-in adapters for popular WebView plugins (auto-detected at startup):

PluginDefine SymbolLicense
UniWebViewUNIWEBVIEWPaid
Vuplex 3D WebViewVUPLEX_WEBVIEWPaid
gree/unity-webviewUNITY_WEBVIEW_GREEFree (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

AspectDetail
siteKeyPublic — safely distributed via GET /api/config
secretKeyPrivate — stored as Workers Secret TURNSTILE_SECRET, never exposed
Token one-useCloudflare auto-invalidates tokens after siteverify
Action verificationPrevents token reuse across endpoints (signup token can't be used for signin)
Service Key bypassServer 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

ModeBehavior on Turnstile API FailureUse Case
open (default)Allow request through, log warningGeneral apps (availability first)
closedReject with 503Finance, 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

# wrangler.test.toml — omit captcha config / CAPTCHA_SITE_KEY

Cloudflare Test Keys

PurposesiteKeysecretKey
Always passes1x00000000000000000000AA1x0000000000000000000000000000000AA
Always fails2x00000000000000000000AB2x0000000000000000000000000000000AB
Forces interactive3x00000000000000000000FF3x0000000000000000000000000000000FF
// 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.