@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.

● v3.0.0 MIT Angular 21 · 22 reCAPTCHA v3 Turnstile
Live demo

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.

cloudflare turnstile Open in a new tab ↗

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.

google recaptcha v3 Open in a new tab ↗

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.

Usage

One provider, one subscription

install
npm i @angx/ngx-easy-captcha
component.ts
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.

API

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.

← All my open source work