Start in Observe on one route. Use the deployment URL and installed package version supplied for your workspace. Historical host examples and source capabilities do not establish a live service or published release.
Warning
Use the unpublished reviewed kit candidate for these server examples, not npm
0.3.0. Normal protected browser actions require hosted live Gate authority.
Legacy CAPTCHA shims preserve callback/form compatibility only during bounded
migration; they are not production browser authority. Hidden response fields
alone cannot replace cookie/request proof. Complete the first-party hosted
rollout before removing the previous production control.
Migration Approaches
There are two ways to migrate from reCAPTCHA to MANDATE:
| Approach | Effort | Description |
|---|---|---|
| Clean migration | Medium | Remove reCAPTCHA entirely, integrate MANDATE directly |
| Shim migration | Low | Drop-in compatibility layer that maps reCAPTCHA APIs to MANDATE |
Clean Migration
Remove the reCAPTCHA Script
Remove the existing reCAPTCHA script tag:
<!-- Remove this -->
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>Add the MANDATE Script
<!-- Add this -->
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>Update Frontend Token Retrieval
Replace grecaptcha.execute() calls with the MANDATE token function:
grecaptcha.ready(function() {
grecaptcha.execute("SITE_KEY", { action: "submit" }).then(function(token) {
document.getElementById("token-field").value = token;
});
});await fetch("/api/action", { method: "POST" });
// Production same-origin protected requests carry Mndt-Sess automatically.See Frontend Integration for the same-origin request-boundary contract.
Update Backend Verification
Replace the reCAPTCHA siteverify API call with local MANDATE SDK verification:
// Network call to Google
const response = await fetch(
"https://www.google.com/recaptcha/api/siteverify",
{
method: "POST",
body: new URLSearchParams({
secret: RECAPTCHA_SECRET,
response: token,
}),
}
);
const data = await response.json();
if (!data.success || data.score < 0.5) {
// Block request
}import { validateHostedRequest } from "@mandate-security/node";
const result = await validateHostedRequest(req, {
siteSecret: process.env.MANDATE_SITE_SECRET,
siteKey: process.env.MANDATE_SITE_KEY,
apiKey: process.env.MANDATE_API_KEY,
gateUrl: process.env.MANDATE_GATE_URL,
credentialMode: "cookie", credentialCookieName: "Mndt-Sess",
highValue: true, origin: "https://app.example.com",
});
if (!result.success) {
// Keep the protected action denied. Prefer a framework adapter so
// authenticated cookie/companion/native updates are applied too.
}Remove reCAPTCHA Dependencies
- Delete the reCAPTCHA secret key from environment variables
- Remove any reCAPTCHA-specific CSS (e.g., hiding the badge)
- Remove
grecaptchatype definitions if using TypeScript
Shim Migration
The shim provides a compatibility layer that maps reCAPTCHA API calls to MANDATE. Use this when you want minimal code changes or need to migrate incrementally.
Swap Script Tags
Replace the reCAPTCHA script with the MANDATE script and shim:
<!-- Remove this -->
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<!-- Add these (order matters) -->
<script async src="https://gate.x-lock.dev/g.js?k=YOUR_SITE_KEY"></script>
<script src="https://gate.x-lock.dev/compat/mandate-shim-core.js"></script>
<script src="https://gate.x-lock.dev/compat/recaptcha-shim.js"></script>Info
Shims are served from the gate at /compat/*. Load order: g.js → mandate-shim-core.js → provider shim. The core reads the legacy compatibility bridge and never calls polymorphic g.js globals. Enable MANDATE_TOKEN_BRIDGE=legacy only in non-production shim migration tests; production rejects page-visible token bridges.
Verify Existing Code Works
The shim provides the following grecaptcha API surface. Your existing frontend code should work without changes:
| reCAPTCHA API | Shim Behavior |
|---|---|
grecaptcha.ready(callback) | Calls callback when MANDATE is ready |
grecaptcha.execute(siteKey, options) | Returns MANDATE token via Promise |
grecaptcha.getResponse() | Returns current MANDATE token |
grecaptcha.render(container, options) | No-op (MANDATE is invisible) |
grecaptcha.reset() | Triggers token refresh |
Update Backend Verification
The shim only covers the frontend. You must still replace the backend siteverify call:
// Replace Google siteverify with the MANDATE SDK
import { validateHostedRequest } from "@mandate-security/node";
const result = await validateHostedRequest(req, {
siteSecret: process.env.MANDATE_SITE_SECRET,
siteKey: process.env.MANDATE_SITE_KEY,
apiKey: process.env.MANDATE_API_KEY,
gateUrl: process.env.MANDATE_GATE_URL,
credentialMode: "cookie", credentialCookieName: "Mndt-Sess",
highValue: true, origin: "https://app.example.com",
});
if (!result.success) {
// Keep the protected action denied. Prefer a framework adapter so
// authenticated cookie/companion/native updates are applied too.
}Info
The shim also populates hidden form fields named g-recaptcha-response for compatibility with forms that submit tokens via POST body. You can read the token from either location during migration.
Remove the Shim (Optional)
Once migration is confirmed stable, remove the shim scripts, disable MANDATE_TOKEN_BRIDGE=legacy, and move production browser routes to first-party cookie delivery and hosted validation (see Frontend Integration).
Key Differences
| Feature | reCAPTCHA | MANDATE |
|---|---|---|
| User interaction | Sometimes (v2 checkbox/puzzle) | Never |
| Backend verification | API call to Google | Local cryptographic check |
| Token delivery | Hidden form field or JS callback | HttpOnly Mndt-Sess in production; X-Mndt only for compatibility |
| Latency | Network round-trip | No MANDATE round trip for compatible local verification |
| Privacy | Data sent to Google | No third-party data sharing |
| Score range | 0.0 - 1.0 | 0.0 - 1.0 |
Warning
reCAPTCHA Enterprise customers: the shim does not support Enterprise-specific features like grecaptcha.enterprise.execute(). Use the clean migration approach instead.