npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@silverassist/recaptcha

v0.2.1

Published

Google reCAPTCHA v3 integration for Next.js applications with Server Actions support

Readme

@silverassist/recaptcha

Google reCAPTCHA v3 integration for Next.js applications with Server Actions support.

npm version License

Features

  • Client Component: RecaptchaWrapper for automatic token generation
  • Server Validation: validateRecaptcha function for Server Actions
  • TypeScript Support: Full type definitions included
  • Next.js Optimized: Works with App Router and Server Actions
  • Auto Token Refresh: Tokens refresh automatically before expiration
  • Graceful Degradation: Works in development without credentials
  • Configurable Thresholds: Custom score thresholds per form
  • Lazy Loading: Optional lazy loading for better performance
  • Singleton Script Loading: Prevents duplicate script loads across multiple forms

Installation

npm install @silverassist/recaptcha
# or
yarn add @silverassist/recaptcha
# or
pnpm add @silverassist/recaptcha

Setup

1. Get reCAPTCHA Keys

  1. Go to Google reCAPTCHA Admin
  2. Create a new site with reCAPTCHA v3
  3. Get your Site Key (public) and Secret Key (private)

2. Add Environment Variables

# .env.local
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=your_site_key_here
RECAPTCHA_SECRET_KEY=your_secret_key_here

Usage

Client Component

Add RecaptchaWrapper inside your form:

"use client";

import { RecaptchaWrapper } from "@silverassist/recaptcha";

export function ContactForm() {
  return (
    <form action={submitForm}>
      <RecaptchaWrapper action="contact_form" />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

Server Action

Validate the token in your Server Action:

"use server";

import { validateRecaptcha, getRecaptchaToken } from "@silverassist/recaptcha/server";

export async function submitForm(formData: FormData) {
  // Get and validate reCAPTCHA token
  const token = getRecaptchaToken(formData);
  const recaptcha = await validateRecaptcha(token, "contact_form");

  if (!recaptcha.success) {
    return { success: false, message: recaptcha.error };
  }

  // Process form data...
  const email = formData.get("email");
  const message = formData.get("message");

  // Your form processing logic here

  return { success: true };
}

⚠️ Important: Custom FormData

RecaptchaWrapper injects a hidden input field containing the reCAPTCHA token. If your form handler creates a custom FormData object, you must ensure the hidden token is included.

❌ This will fail (token is missing):

"use client";

import { RecaptchaWrapper } from "@silverassist/recaptcha";
import { submitForm } from "./actions"; // Your server action

export function ContactForm() {
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // ❌ Creating empty FormData - hidden reCAPTCHA input is NOT included!
    const formData = new FormData();
    formData.set("email", "[email protected]");
    formData.set("message", "Hello");
    
    await submitForm(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <RecaptchaWrapper action="contact_form" />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

✅ Recommended: Pass form element to capture all inputs

"use client";

import { RecaptchaWrapper } from "@silverassist/recaptcha";
import { submitForm } from "./actions"; // Your server action

export function ContactForm() {
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // ✅ Pass form element - captures ALL inputs including hidden reCAPTCHA token
    const formData = new FormData(e.currentTarget);
    
    await submitForm(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <RecaptchaWrapper action="contact_form" />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

✅ Alternative: Start with form element, then modify

"use client";

import { RecaptchaWrapper } from "@silverassist/recaptcha";
import { submitForm } from "./actions"; // Your server action

export function ContactForm() {
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // ✅ Start with form element (includes hidden token)
    const formData = new FormData(e.currentTarget);
    
    // Then add/override specific fields
    formData.set("customField", "customValue");
    formData.set("timestamp", Date.now().toString());
    
    await submitForm(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <RecaptchaWrapper action="contact_form" />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

Performance Optimization: Lazy Loading

The lazy prop enables lazy loading of the reCAPTCHA script, which defers loading until the form becomes visible in the viewport. This significantly improves initial page load performance.

Performance Benefits

Note: These metrics are approximate values measured on production websites using Google reCAPTCHA v3. Actual performance improvements will vary based on network conditions, device capabilities, and page complexity.

| Metric | Without Lazy Loading | With Lazy Loading | Improvement | |--------|---------------------|-------------------|-------------| | Initial JS | 320KB+ | 0 KB (until visible) | -320KB | | TBT (Total Blocking Time) | ~470ms | ~0ms (deferred) | -470ms | | TTI (Time to Interactive) | +2-3s | Minimal impact | -2-3s |

Basic Lazy Loading

Enable lazy loading by adding the lazy prop:

"use client";

import { RecaptchaWrapper } from "@silverassist/recaptcha";

export function ContactForm() {
  return (
    <form action={submitForm}>
      {/* Script loads only when form is near viewport */}
      <RecaptchaWrapper action="contact_form" lazy />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">Send</button>
    </form>
  );
}

Custom Root Margin

Control when the script loads with the lazyRootMargin prop (default: "200px"):

// Load script earlier (400px before entering viewport)
<RecaptchaWrapper action="contact_form" lazy lazyRootMargin="400px" />

// Load script later (load only when fully visible)
<RecaptchaWrapper action="contact_form" lazy lazyRootMargin="0px" />

// Load with negative margin (load only after scrolling past)
<RecaptchaWrapper action="contact_form" lazy lazyRootMargin="-100px" />

Best Practices

1. Use lazy loading for below-the-fold forms

// Hero form (above the fold) - load immediately
<RecaptchaWrapper action="hero_signup" />

// Footer form (below the fold) - lazy load
<RecaptchaWrapper action="footer_contact" lazy />

2. Multiple forms on the same page

The package automatically uses singleton script loading, so the script is only loaded once even with multiple forms:

export function MultiFormPage() {
  return (
    <>
      {/* First form triggers script load */}
      <RecaptchaWrapper action="newsletter" lazy />
      
      {/* Second form reuses the same script */}
      <RecaptchaWrapper action="contact" lazy />
      
      {/* Third form also reuses the script */}
      <RecaptchaWrapper action="feedback" lazy />
    </>
  );
}

3. Adjust root margin based on form position

// Form near top - smaller margin for faster load
<RecaptchaWrapper action="signup" lazy lazyRootMargin="100px" />

// Form far down page - larger margin to load in advance
<RecaptchaWrapper action="newsletter" lazy lazyRootMargin="500px" />

API Reference

RecaptchaWrapper

Client component that loads reCAPTCHA and generates tokens.

<RecaptchaWrapper
  action="contact_form"      // Required: action name for analytics
  inputName="recaptchaToken" // Optional: hidden input name (default: "recaptchaToken")
  inputId="recaptcha-token"  // Optional: hidden input id
  siteKey="..."              // Optional: override env variable
  refreshInterval={90000}    // Optional: token refresh interval in ms (default: 90000)
  onTokenGenerated={(token) => {}} // Optional: callback when token is generated
  onError={(error) => {}}    // Optional: callback on error
  lazy={false}               // Optional: enable lazy loading (default: false)
  lazyRootMargin="200px"     // Optional: IntersectionObserver rootMargin (default: "200px")
/>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | action | string | Required | Action name for reCAPTCHA analytics (e.g., "contact_form", "signup") | | inputName | string | "recaptchaToken" | Name attribute for the hidden input field | | inputId | string | "recaptcha-token" | ID attribute for the hidden input field | | siteKey | string | process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY | Override the site key from environment variable | | refreshInterval | number | 90000 | Token refresh interval in milliseconds (90 seconds) | | onTokenGenerated | (token: string) => void | undefined | Callback invoked when a new token is generated | | onError | (error: Error) => void | undefined | Callback invoked when an error occurs | | lazy | boolean | false | Enable lazy loading to defer script until form is visible | | lazyRootMargin | string | "200px" | IntersectionObserver rootMargin (used when lazy is true) |

validateRecaptcha

Server-side token validation function.

const result = await validateRecaptcha(
  token,          // Token from form
  "contact_form", // Expected action (optional)
  {
    scoreThreshold: 0.5, // Minimum score (default: 0.5)
    secretKey: "...",    // Override env variable
    debug: true,         // Enable debug logging
  }
);

// Result type:
// {
//   success: boolean,
//   score: number,
//   error?: string,
//   skipped?: boolean,
//   rawResponse?: RecaptchaVerifyResponse
// }

isRecaptchaEnabled

Check if reCAPTCHA is configured.

import { isRecaptchaEnabled } from "@silverassist/recaptcha/server";

if (isRecaptchaEnabled()) {
  // Validate token
} else {
  // Skip validation (development)
}

getRecaptchaToken

Extract token from FormData.

import { getRecaptchaToken } from "@silverassist/recaptcha/server";

const token = getRecaptchaToken(formData);
const token = getRecaptchaToken(formData, "customFieldName");

Score Thresholds

reCAPTCHA v3 returns a score from 0.0 to 1.0:

| Score | Meaning | |-------|---------| | 1.0 | Very likely human | | 0.7+ | Likely human | | 0.5 | Default threshold | | 0.3- | Suspicious | | 0.0 | Very likely bot |

Adjust threshold based on form sensitivity:

// Standard forms
await validateRecaptcha(token, "contact", { scoreThreshold: 0.5 });

// Sensitive forms (payments, account creation)
await validateRecaptcha(token, "payment", { scoreThreshold: 0.7 });

// Low-risk forms (newsletter signup)
await validateRecaptcha(token, "newsletter", { scoreThreshold: 0.3 });

Subpath Imports

You can import from specific subpaths for better tree-shaking:

// Main exports (client + server + types)
import { RecaptchaWrapper, validateRecaptcha } from "@silverassist/recaptcha";

// Client only
import { RecaptchaWrapper } from "@silverassist/recaptcha/client";

// Server only
import { validateRecaptcha, getRecaptchaToken, isRecaptchaEnabled } from "@silverassist/recaptcha/server";

// Types only
import type { RecaptchaValidationResult, RecaptchaWrapperProps } from "@silverassist/recaptcha/types";

// Constants only
import { DEFAULT_SCORE_THRESHOLD, RECAPTCHA_CONFIG } from "@silverassist/recaptcha/constants";

Development

In development, when RECAPTCHA_SECRET_KEY is not set, validation is skipped and forms work normally. This allows testing without reCAPTCHA credentials.

const result = await validateRecaptcha(token, "test");
// Returns: { success: true, score: 1, skipped: true }

TypeScript

Full TypeScript support with exported types:

import type {
  RecaptchaWrapperProps,
  RecaptchaValidationResult,
  RecaptchaVerifyResponse,
  RecaptchaConfig,
  RecaptchaValidationOptions,
} from "@silverassist/recaptcha";

License

Polyform Noncommercial License 1.0.0

Links