nearlyin
v0.2.0
Published
Waitlist form (React), server helpers and webhook verification for NearlyIn
Maintainers
Readme
nearlyin
The waitlist form, the server helpers and the webhook check for NearlyIn: collect sign-ups, confirm addresses, invite people, and let your own sign-up check who is invited.
Full documentation: nearlyin.com/docs
npm install nearlyinYou need a product in the NearlyIn dashboard. It gives you a public key (ni_pk_…) for the browser and a secret key (ni_sk_…) for your server, each for test mode and for live.
The form
import { WaitlistForm } from "nearlyin";
export function Signup() {
return <WaitlistForm productKey="ni_pk_test_…" locale="en" layout="pill" hideLabel />;
}The component brings its own styles and loads nothing from other servers. It posts to https://nearlyin.com unless you pass another serviceUrl. Allow the address of your page in the dashboard under "Allowed origins"; a pattern such as https://*-team.vercel.app covers preview deployments. React 18 or newer.
| Prop | What it does |
| --- | --- |
| layout | "inline" (default): address and button side by side, wrapping where there is no room. "stacked": the button below, both full width. "pill": both in one round field, stacked on narrow screens. |
| radius | Rounding of field and button, as pixels (12) or any CSS length. Default 8, fully round for pill. |
| hideLabel | Hides the label from sight; screen readers still read it. |
| colors | accent, accentHover, accentText, text, inputBackground, inputText, border, error. Any CSS colour value. |
| texts | Your own wording, per text. locale ("de" or "en") picks the defaults. |
| unstyled | Leaves out the built-in styles. A hidden label and the trap for bots still work. |
| className | A class of your own on the outer element. |
| referral, source | If your page knows the referral code and the source itself. |
| onSuccess | A next step of your own once the address is accepted. |
These class names are stable, with or without unstyled, so your own styles can hang on them:
.wlf the outer element, with data-layout and data-status (idle, submitting, success, error)
.wlf-form the form
.wlf-label the label
.wlf-row address and button
.wlf-input the address field
.wlf-button the button
.wlf-error the error message (empty while there is none)
.wlf-status "check your inbox", after sending
.wlf-title its heading
.wlf-text its textIf a sign-up fails, the browser's console says why: a blocked connection (see the CSP below), or an origin that is missing under "Allowed origins" (origin_not_allowed). With a test key the form shows the latter itself.
Content-Security-Policy
If your site sends one, add NearlyIn to these directives:
connect-src https://nearlyin.com
form-action https://nearlyin.comLetting invited people in
The invitation mail links to your product with ?invite=<code>. Set the product's link in the dashboard to the page that checks it, usually your sign-up page. On your server:
import { admitInvitedAccount, isInvited, redeemInvite, verifyInvite } from "nearlyin/server";
// When the page opens: whose code is this? Does not use it up.
const email = await verifyInvite(searchParams.invite); // "[email protected]" or null
// When your server creates the account: works exactly once.
const invited = await redeemInvite(code, { idempotencyKey: signupId });
if (!invited) throw new Error("This invitation is no longer valid.");
// Without a code: is this address allowed in?
await isInvited("[email protected]");With a login that creates the account itself
Clerk, Auth0, Supabase Auth or Auth.js create the account; your app has no moment of its own to redeem the code in. Hand the code on with the provider's sign-up (Clerk's unsafeMetadata, Supabase's user_metadata, a cookie) and check it when the new account makes its first request:
import { admitInvitedAccount } from "nearlyin/server";
const admitted = await admitInvitedAccount({
code: user.unsafeMetadata.invite,
verifiedEmails: user.emailAddresses.filter((a) => a.verification?.status === "verified").map((a) => a.emailAddress),
attemptId: user.id,
});
if (!admitted) redirect("/sign-up?invite=required");It returns true only when the code is a valid invitation for one of the verified addresses (case does not matter), and redeems it then. It returns false without a code, for an unknown, expired or used code, and for a code of another address, which stays unused, so a forwarded link does not let a stranger in. It never throws: when NearlyIn cannot be asked it returns false and passes the error to onError (default console.error). attemptId makes a retry within 24 hours get the first answer; use the account's id. Recipes for Clerk, Supabase Auth and Auth.js: nearlyin.com/docs/auth/clerk.
Configuration
Set NEARLYIN_SECRET_KEY in your server's environment; that is all. NEARLYIN_URL is only needed when NearlyIn runs somewhere else than https://nearlyin.com. You can also pass secretKey and serviceUrl to each call. The secret key must never reach the browser: the helpers refuse to run there.
nearlyin/server also manages the list: listEntries, getEntry, inviteEntry, inviteNext, reinviteEntry, moveUpEntry and deleteEntry.
Webhooks
import { verifyWebhook } from "nearlyin/webhooks";
const event = await verifyWebhook(rawBody, request.headers.get("NearlyIn-Signature"), process.env.NEARLYIN_WEBHOOK_SECRET!);
// event.type: "entry.created" | "entry.confirmed" | "entry.invited" | "entry.redeemed" | ...It uses the Web Crypto API and runs in Node 18+, Deno, Bun and edge runtimes. Pass the body exactly as it arrived, before any JSON parsing.
Without React
examples/form.html in this package is a plain HTML form. Or use the script from the dashboard; it needs no package at all:
<script src="https://nearlyin.com/embed.js" data-key="ni_pk_live_…" async></script>In React or Next.js, which move script tags into <head>, mark the place with a placeholder and load the script once, anywhere:
<div data-nearlyin="ni_pk_live_…" data-variant="compact" data-layout="pill"></div>
<script src="https://nearlyin.com/embed.js" async></script>The script takes data-layout, data-radius and data-hide-label like the props above. For a CSP, it needs script-src and frame-src https://nearlyin.com (and connect-src, for its own button in your colour).
