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

@humaan/payload-plugin-form-builder

v0.0.6

Published

Self-contained Payload form builder plugin with admin conveniences and frontend form components.

Readme

Payload Plugin Form Builder

Self-contained Payload 3 form builder plugin. It wraps @payloadcms/plugin-form-builder, adds form field authoring conveniences in the admin UI, stores submitted values encrypted by default, and exports frontend React components for rendering forms.

Install

pnpm add @humaan/payload-plugin-form-builder @payloadcms/plugin-form-builder @payloadcms/ui

Payload, @payloadcms/plugin-form-builder, @payloadcms/ui, React, and React DOM are peer dependencies. Keep payload and all @payloadcms/* packages pinned to the same exact version in the consuming project. BotID and React Email are optional peers and are only needed when the matching plugin options are enabled.

Basic Usage

import { buildConfig } from 'payload'
import { payloadPluginFormBuilder } from '@humaan/payload-plugin-form-builder'

export default buildConfig({
  plugins: [
    payloadPluginFormBuilder({
      redirectRelationships: ['pages'],
      contentFormBlock: true,
    }),
  ],
})

This registers forms and form-submissions, enables the custom form field blocks, groups both collections under Forms in the admin sidebar, encrypts submitted field labels/values, and exposes the formatted submission data admin view.

Options

payloadPluginFormBuilder({
  disabled?: boolean
  adminGroup?: string | false
  redirectRelationships?: CollectionSlug[]
  contentFormBlock?: boolean | Partial<Block>
  deliveryLogs?: boolean | {
    emails?: boolean
    retentionDays?: number | false
    adminGroup?: string | false
  }
  hubspot?: {
    portalId: string
    timeoutMs?: number
    token?: string
    tokenEnv?: string
    trackingContext?: boolean
    uploads?: {
      allowedMimeTypes?: string[]
      maxFiles?: number
      maxFileSize?: number
      maxTotalSize?: number
    }
  }
  webhooks?: boolean | {
    enabled?: boolean
    timeoutMs?: number
    maxResponseBodyLength?: number
  }
  submissions?: {
    encrypt?: boolean
    botProtection?:
      | false
      | 'botid'
      | { provider: 'botid' }
      | {
          provider: 'recaptcha'
          secretKey?: string
          secretKeyEnv?: string
          tokenFieldName?: string
        }
      | {
          provider: 'turnstile'
          secretKey?: string
          secretKeyEnv?: string
          tokenFieldName?: string
        }
    useFormattedAdminView?: boolean
  }
  email?: {
    template?: false | 'default'
  }
  formFieldHooks?: {
    blocks?: {
      all?: PayloadPluginFormBuilderFormFieldBlockHook
      [slug]?: PayloadPluginFormBuilderFormFieldBlockHook
    }
    previews?: {
      all?: PayloadPluginFormBuilderFormFieldPreviewHook
      [slug]?: PayloadPluginFormBuilderFormFieldPreviewHook
    }
  }
  formOverrides?: PluginConfig['formOverrides']
  formSubmissionOverrides?: PluginConfig['formSubmissionOverrides']
})

Defaults:

  • adminGroup: 'Forms'
  • submissions.encrypt: true
  • submissions.useFormattedAdminView: true
  • submissions.botProtection: false
  • email.template: false
  • deliveryLogs: false
  • deliveryLogs.emails: true
  • deliveryLogs.retentionDays: 90
  • hubspot: not configured
  • hubspot.timeoutMs: 30000
  • hubspot.tokenEnv: 'HUBSPOT_TOKEN'
  • hubspot.trackingContext: true
  • hubspot.uploads.allowedMimeTypes: []
  • hubspot.uploads.maxFiles: 5
  • hubspot.uploads.maxFileSize: 5242880 (5 MiB)
  • hubspot.uploads.maxTotalSize: 15728640 (15 MiB)
  • webhooks: false
  • webhooks.timeoutMs: 10000
  • webhooks.maxResponseBodyLength: 4000

Redirect Relationships

Set redirectRelationships to the collection slugs that can be selected as internal redirect targets when an editor sets a form's confirmation type to redirect.

payloadPluginFormBuilder({
  redirectRelationships: ['pages'],
})

This only configures the Payload admin field. The frontend renderer can redirect to custom URLs by default, but relationship redirects need a resolveRedirect callback so the consuming app can map the selected document to the correct public URL.

Per-Site Field and Preview Hooks

Use formFieldHooks when a site needs to adjust the generated form field blocks before Payload receives them. Global all hooks run first, slug-specific hooks run second, and preview hooks run after block hooks. formOverrides.fields still runs last for collection-level changes.

import type { FieldHook } from 'payload'

import { payloadPluginFormBuilder } from '@humaan/payload-plugin-form-builder'

const normalizeSiteCode: FieldHook = ({ value }) => String(value || '').toUpperCase()

payloadPluginFormBuilder({
  formFieldHooks: {
    blocks: {
      text: ({ block }) => ({
        ...block,
        fields: [
          ...block.fields,
          {
            name: 'siteCode',
            type: 'text',
            hooks: {
              beforeChange: [normalizeSiteCode],
            },
          },
        ],
      }),
    },
    previews: {
      all: ({ preview, slug }) =>
        typeof preview === 'string'
          ? preview
          : {
              alt: `${slug} field preview`,
              url: preview?.url || `/admin/form-field-previews/${slug}.png`,
            },
      textarea: () => ({
        alt: 'Long answer field preview',
        url: '/admin/form-field-previews/long-answer.png',
      }),
    },
  },
})

To enable BotID submission protection, install botid, set submissions.botProtection: 'botid', and import the package instrumentation in your app:

import '@humaan/payload-plugin-form-builder/instrumentation-client'

You can also use the object form:

payloadPluginFormBuilder({
  submissions: {
    botProtection: {
      provider: 'botid',
    },
  },
})

To enable Cloudflare Turnstile submission protection, set submissions.botProtection to the Turnstile provider and configure TURNSTILE_SECRET_KEY in the server environment:

payloadPluginFormBuilder({
  submissions: {
    botProtection: {
      provider: 'turnstile',
    },
  },
})

Then pass the matching public site key to the frontend form, or expose it as NEXT_PUBLIC_TURNSTILE_SITE_KEY:

import { Form } from '@humaan/payload-plugin-form-builder/client'

export function ContactForm({ form }) {
  return (
    <Form
      botProtection={{
        provider: 'turnstile',
        siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY,
      }}
      form={form}
    />
  )
}

Turnstile defaults to TURNSTILE_SECRET_KEY for server verification and __turnstileToken for the hidden token field. If you override tokenFieldName server-side, pass the same value to <Form />.

To enable Google reCAPTCHA submission protection, set submissions.botProtection to the reCAPTCHA provider and configure RECAPTCHA_SECRET_KEY in the server environment:

payloadPluginFormBuilder({
  submissions: {
    botProtection: {
      provider: 'recaptcha',
    },
  },
})

Then pass the matching public site key to the frontend form, or expose it as NEXT_PUBLIC_RECAPTCHA_SITE_KEY:

import { Form } from '@humaan/payload-plugin-form-builder/client'

export function ContactForm({ form }) {
  return (
    <Form
      botProtection={{
        provider: 'recaptcha',
        siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
      }}
      form={form}
    />
  )
}

reCAPTCHA defaults to RECAPTCHA_SECRET_KEY for server verification and __recaptchaToken for the hidden token field. If you override tokenFieldName server-side, pass the same value to <Form />.

To wrap outgoing form-builder emails in the default email shell, install react-email and set email.template: 'default'.

Add-On Features

Delivery logs, webhooks, and HubSpot delivery are separate opt-in add-ons.

Set deliveryLogs: true to add a delivery-logs collection, a delivery log join on form submissions, and email delivery logging:

payloadPluginFormBuilder({
  deliveryLogs: true,
})

Delivery logs track attempted, dispatched, sent, and failed deliveries. Email logging is enabled by default when deliveryLogs is enabled:

payloadPluginFormBuilder({
  deliveryLogs: {
    emails: false,
  },
})

Set webhooks: true to add per-form webhooks and send configured form submission webhooks:

payloadPluginFormBuilder({
  webhooks: true,
})

Form webhook URLs must use https://. Custom webhook header values are encrypted at rest and can use tokens such as {{ form.title }}, {{ submission.id }}, {{ request.sourceUrl }}, and {{ fields.email }}.

Enable both add-ons to record webhook delivery attempts in delivery-logs:

payloadPluginFormBuilder({
  deliveryLogs: true,
  webhooks: true,
})

HubSpot

Configure hubspot with an object to add per-form HubSpot delivery:

payloadPluginFormBuilder({
  deliveryLogs: true,
  hubspot: {
    portalId: '12345678',
    tokenEnv: 'HUBSPOT_TOKEN',
  },
})

hubspot has no true shorthand: portalId is required. token, tokenEnv, and trackingContext are optional. ipAddress is deliberately not a plugin option; when tracking context is enabled, the delivery hook derives it from the server-side request.

To find the IDs, open the HubSpot form's embed code. It contains the account ID as portalId and the form ID as formId:

hbspt.forms.create({
  portalId: '12345678',
  formId: '11111111-2222-3333-4444-555555555555',
})

Use portalId in the plugin config and use formId as the per-form HubSpot Form GUID in Payload. Neither value is a secret; both are present in HubSpot's public browser embed. A private app token is a secret and must remain server-side.

The token is optional and never authenticates submission delivery. Configuring token directly, or exposing one through tokenEnv (which defaults to HUBSPOT_TOKEN), only enables config-time reads of the HubSpot form definition. Those checks verify that the form exists, flag mappings to properties that are not on the form, flag required properties that are not mapped, and detect the two fatal unsupported configurations below. A token also lets the HubSpot Form GUID field suggest the portal's forms by name. Saving remains available if validation fails or HubSpot is unavailable.

Without a token, submissions use the same unauthenticated delivery path, but the admin UI cannot perform those checks. It records that the configuration was not checked; you must verify the form and mappings in HubSpot yourself. The form GUID field stays a plain text input in that case.

In each Payload form's HubSpot tab:

  1. Enable HubSpot delivery and enter the HubSpot Form GUID, or pick one of the suggested forms.
  2. Explicitly map each Payload field to a property on that HubSpot form.
  3. Keep Tracking Page URL set to Relative unless the production site domain is connected to the HubSpot portal.

HubSpot delivery does not support either of these HubSpot form configurations. Both are fatal: every submission from this plugin will fail.

  • HubSpot reCAPTCHA enabled
  • HubSpot GDPR or consent options enabled

Use this plugin's own bot-protection options instead of enabling reCAPTCHA on the HubSpot form. Consent delivery is outside this integration's current scope.

Tracking context warning

trackingContext defaults to true. The plugin can send hutk, pageName, pageUrl, and the server-derived ipAddress. hutk is only available when the customer site installs HubSpot's tracking script, which sets the hubspotutk cookie. This plugin neither installs nor controls that script.

If the frontend receives a form through a Payload field select, include hubspot in the selection. The renderer reads form.hubspot.enabled to decide whether to capture HubSpot tracking context; omitting the group silently disables that capture.

Keep pageUrl relative by default. If Absolute sends a page URL whose domain is not connected to the HubSpot portal, HubSpot can silently destroy every submission: it returns 204, creates no record, and the delivery log still reads dispatched. The same setup can pass on localhost, so local testing does not prove that an absolute production URL is safe. Select Absolute only after connecting the production domain in HubSpot.

Delivery logs and Postgres migrations

HubSpot's terminal success status is dispatched, not sent. It means only that HubSpot accepted the request bytes; nothing further is known. The HubSpot channel never writes sent, and a 204 response does not prove that a contact or submission record was created.

The HubSpot channel widens the delivery-logs.channel enum with hubspot, widens delivery-logs.status with dispatched, and adds a nullable request textarea containing a redacted request summary. This package does not ship database migrations. Existing Postgres adopters must generate and apply an application migration for all three schema changes before deploying the HubSpot-enabled config.

File uploads

The HubSpot channel adds a pass-through file field. Files are validated in request memory, sent directly to HubSpot, and never persisted by this plugin. The defaults are 5 MiB per file, 5 files per field, and 15 MiB across one submission. Use hubspot.uploads to change those limits or to restrict the MIME types an editor may allow on a file field.

Frontend Rendering

import { Form } from '@humaan/payload-plugin-form-builder/client'

export function ContactForm({ form }) {
  return <Form form={form} />
}

The renderer is intentionally design-system neutral. It emits stable payload-form-builder__* class names and accepts renderRichText, renderConfirmation, and resolveRedirect callbacks for host-specific rendering.

Use resolveRedirect when forms can redirect to Payload relationship documents:

import { Form } from '@humaan/payload-plugin-form-builder/client'

export function ContactForm({ form }) {
  return (
    <Form
      form={form}
      resolveRedirect={(redirect) => {
        if (
          redirect &&
          typeof redirect === 'object' &&
          'type' in redirect &&
          redirect.type === 'reference' &&
          'reference' in redirect &&
          redirect.reference &&
          typeof redirect.reference === 'object' &&
          'value' in redirect.reference
        ) {
          const document = redirect.reference.value

          if (
            document &&
            typeof document === 'object' &&
            'slug' in document &&
            typeof document.slug === 'string'
          ) {
            return `/${document.slug}`
          }
        }

        return undefined
      }}
    />
  )
}

Additional exports:

  • ContentForm
  • field components such as InputField, TextareaField, SelectField, CheckboxField, CheckboxGroupField, and RadioGroupField
  • createFormSubmission

Development

pnpm generate:importmap
pnpm generate:types
pnpm lint
pnpm test:int
pnpm build

The integration tests use mongodb-memory-server, so they need permission to bind local ports.

Releasing

Features land on main by pull request as usual. To cut a release:

  1. Actions → Version → Run workflow, and pick a bump type (patch, minor, major, prerelease, or custom with an explicit version).

    It bumps package.json, regenerates CHANGELOG.md, and opens a chore(release): vX.Y.Z pull request.

  2. Nothing else to do. humaan-bot opens the pull request, its checks start automatically, and GitHub merges it with the repository's normal squash strategy once all lifecycle gates pass. Failed checks leave the pull request open and unmerged.

  3. CI runs on main, then the Release workflow publishes to npm, tags vX.Y.Z, and creates the GitHub release.

The version bump cannot be pushed to main by CI — the branch ruleset requires status checks a direct push can never satisfy — so it goes through the protected pull request path like any other change. See ADR 0002 for the reasoning.

If a publish fails

Re-run Actions → Release → Run workflow with confirm set to publish. Nothing on main changed, so the same unpublished version is picked up, and the tag is only pushed after a successful publish — a failure never leaves an orphan tag behind.

Do not cut a new version to work around a failed publish. Version will refuse to bump while the version on main is missing from npm, because that would skip a version number.

Ticking dry_run on Release reports the version and dist-tag it would use and publishes nothing.

Bumping locally

Version runs the same script you can run yourself, if you would rather prepare the release branch by hand:

pnpm release:bump patch

It rewrites package.json, regenerates CHANGELOG.md, and prints the branch and commit steps. The pull request title must be exactly chore(release): vX.Y.Zvalidate requires a conventional commit, and Release keys off that prefix to decide whether a merge is a release.

Prerelease versions

A version with a prerelease identifier publishes under a matching dist-tag instead of latest: alpha, beta, rc, and next map to themselves, anything else to next. The prerelease bump type uses the beta identifier.