@angx/ngx-easy-captcha
The "prove you're not a robot" check, added to an Angular app in one line — and switched between Google and Cloudflare by changing a single word.
Both providers, running here
The same form, the same library, the same subscription — wired to Cloudflare below and to Google underneath it. Look for "Token received" in each: that's the verification token arriving, which is the whole point of the library.
Running on Cloudflare's public
test key
(1x00000000000000000000AA), which passes automatically. Because
it never needs to challenge anyone, Turnstile issues the token immediately and draws no checkbox — the
integration is real, it just skips the "I'm not a robot" step. Your own key puts the widget in that gap.
reCAPTCHA v3 never interrupts anyone — there's no puzzle and no checkbox. It scores the visit silently in the background, so the only things you can see are the reCAPTCHA badge in the corner and the token count on the form. Both are real: this panel runs on a live site key registered for this domain.
One provider, one subscription
npm i @angx/ngx-easy-captcha
import { CaptchaProvider, provideNgxEasyCaptcha, NgxEasyCaptchaService, } from '@angx/ngx-easy-captcha'; @Component({ providers: [provideNgxEasyCaptcha({ provider: CaptchaProvider.CloudFlare, siteKey: '0x4AAA...', initializer: 'turnstile-captcha', })], /* ... */ }) export class SignInComponent { private readonly captcha = inject(NgxEasyCaptchaService); readonly token = signal(''); constructor() { this.captcha.$ .pipe(takeUntilDestroyed()) .subscribe(t => this.token.set(t)); } }
Switching provider is one word. Change
CaptchaProvider.CloudFlare to
.Google and the same subscription keeps working — the service
hides the difference between the two vendors' very different APIs.
The script loads on demand. Neither vendor's JavaScript is in your bundle or your
index.html; it's injected only when a component that needs it is created.
And it cleans up. When the injector that provided the service is destroyed, the
script tag, the rendered widget and the global it left on window are all removed —
cleanup doesn't depend on you remembering to unsubscribe.
The initializer means different things per provider: for
Turnstile it's the id prefix of the elements that should host a widget, so several forms on one page
each get their own. For reCAPTCHA it's the action name reported to Google.
Configuration
provideNgxEasyCaptcha({ ... })
| Option | Type | Required | What it does |
|---|---|---|---|
| provider | CaptchaProvider | yes | CaptchaProvider.CloudFlare or CaptchaProvider.Google. The only line
that changes when you switch vendors. |
| siteKey | string | yes | Your public site key from the provider's dashboard. Public by design — the matching secret key stays on your server. |
| initializer | string | yes | Turnstile: the id prefix of the host elements. reCAPTCHA: the action name. |
| Service member | Type | What it does | |
|---|---|---|---|
| $ | Observable<string> | — | Emits the verification token as it's issued. Send it to your backend to verify. |
| refresh() | void | — | Re-runs the challenge — e.g. after a failed submit. |
A token only proves the check ran in the browser. Always verify it server-side with your secret key before trusting a submission — this library handles the front end, not the verification.
Free to use, MIT licensed
Use it, fork it, ship it. Issues and pull requests are welcome.