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

@backendfree/inbox

v0.1.0

Published

A website's contact form, sent to its BackendFree inbox past the spam checks, and its messages read back.

Readme

@backendfree/inbox

A website's contact form, sent to its inbox on the dashboard, and the messages read back. Built on @backendfree/core.

npm install @backendfree/core @backendfree/inbox
import { Client } from '@backendfree/core';
import { Inbox, formTimer } from '@backendfree/inbox';

const inbox = new Inbox(
  new Client({ origin: 'https://backendfree.com', key: 'pk_live_...' }),
);

const opened = formTimer();                    // when the form appears

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const data = new FormData(form);
  await inbox.send({
    name: String(data.get('name')),
    email: String(data.get('email')),
    message: String(data.get('message')),
    page: location.pathname,
    honeypot: String(data.get('website') ?? ''),
    filled_in_seconds: opened(),
  });
  showThanks();
});

The owner is emailed each message, with your visitor's address as Reply-To, and finds them all under Inbox on the dashboard. Switch Inbox on for the website first; until then /v1/messages answers 404.

Keeping spam out

Every message passes these checks, on the platform's side, before anybody sees it:

  • A hidden field. Add an input people never see and send whatever it holds as honeypot. A script filling every field fills it too, and that message is answered as received and not kept.
  • How fast it was sent. formTimer() measures how long the form was open; a form sent faster than a person can fill one in goes to spam.
  • Links. More than a couple of web addresses, or a link written as code, goes to spam.
  • Limits. One visitor address may send a few messages an hour to one website, and a website takes a set number a day. The limits page has the figures.

The hidden field, hidden from people and from screen readers but not from a script reading the page:

<div aria-hidden="true" style="position: absolute; left: -10000px">
  <label>Leave this empty <input name="website" tabindex="-1" autocomplete="off"></label>
</div>

Spam is kept for a while in the Spam folder, where the owner can move a message back. It is never emailed and never sent to a webhook.

send resolves the same whatever became of the message, so a script learns nothing from the answer. Two refusals throw, because a person can act on them:

| code | means | |---|---| | bot_check_failed | The website asks for Cloudflare's check and the token was missing, spent or refused. Show the check again. | | daily_limit_reached | The website has had its messages for today. Suggest another way to get in touch. |

Cloudflare's bot check

Optional, for a form a script keeps getting past. Make a Turnstile widget in your own Cloudflare account, put its site key in your form, and paste its secret key on the inbox's settings screen. From then on every message has to carry a token Cloudflare accepts:

await inbox.send({
  name, email, message,
  bot_check: String(data.get('cf-turnstile-response') ?? ''),
});

A token works once, so after a refusal reset the widget before sending again. Cloudflare's test keys, which always pass or always fail, try it without a real widget.

From your own server

send works with a secret key as well, from a server action or an API route, for a form that posts to your own server first. The per-visitor limits then see your server rather than the visitor, so apply your own; the daily limit still holds.

Reading messages

Reading needs a secret key, and list and message throw on a publishable one before any request is made: a message is a stranger's name, address and words.

import { verifyWebhook } from '@backendfree/core';

const event = await verifyWebhook({ body: await request.text(), headers, secret });
if (event.name === 'message.received') {
  const message = await inbox.message(event.data.message_id);
  await crm.add(message.email, message.name, message.message);
}

message.received carries the message's id and nothing anybody wrote in it. It is sent when a message arrives in the inbox, or is moved there out of spam.

const { results } = await inbox.list({ folder: 'inbox' });