Skip to content

Kicklace, an AI-powered CRM for micro SaaS

Next.js CRM

One component in your layout sees your visitors. Two calls in your sign-up route say who they are. The lifecycle, the emails and the follow-up after that are Kicklace's.

One component in your layout

The tag is what your website knows: page reads, where somebody came from, and the signup forms it can hear. In a Next.js app it is a component.

app/layout.tsx
import { KicklaceTag } from "kicklace/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <KicklaceTag />
      </body>
    </html>
  );
}

It reads NEXT_PUBLIC_KICKLACE_KEY and NEXT_PUBLIC_KICKLACE_URL, so it needs no props, and it takes publicKey and url when you would rather pass them. It keeps one random id in the browser's own storage and sets no cookies.

Two calls in your sign-up route

The tag cannot know who somebody is. Your route handler can, and these are the two calls that say so.

Once, at the top of the module
import { Kicklace } from "kicklace";

const kl = new Kicklace(process.env.KICKLACE_SECRET_KEY!);
Inside the handler
await kl.identify({ email, anonId, name, fields: { phone } });

await kl.subscribe({
  email,
  list: "product-updates",
  consent: "Send me product updates. I can unsubscribe with one click.",
});

The consent sentence is the point of the second call. Kicklace stores it verbatim beside the subscription as the proof of what the person read, with the day and, if you send it, their IP; without one the call is refused. Keep it in a constant your form and your handler both import, so what somebody agreed to and what you wrote down cannot differ.

anonId is the id the tag keeps in the browser (kl_id). Send it with the sign-up and the visitor who read your pricing page last week and the account created today are one record, with the earliest first touch kept — which is how you find out which campaign really pays.

Payments: pick one

A purchase can reach Kicklace two ways, and running both lands it twice.

  • Kicklace takes Stripe's webhook itself. One address pasted into Stripe → Developers → Webhooks and the signing secret brought back, and there is no route in your app at all. Prefer this: it also reads refunds, trials, renewals and cancellations, which a handler written for one checkout usually does not.
  • Or post it from your own route. If your payment flow already does work of its own when money arrives, add one line to it. The idempotency key is what makes a retry — Stripe's or yours — land once.
  • Not both. Two paths for one payment is two purchases unless they agree on the key, and the honest fix is to choose.
From your own handler
await kl.track("purchase", { email, value: 49, idempotencyKey: invoice.id });

The four names

What a Next.js project needs in its environment, and nothing else.

NEXT_PUBLIC_KICKLACE_KEY
Your public key, which the tag reads. It is meant to be in a page.
KICKLACE_SECRET_KEY
Your secret key, on the server only. Handed one, the tag renders nothing and says why in the console.
NEXT_PUBLIC_KICKLACE_URL
Where the tag is served from, if not the default.
KICKLACE_URL
Where Kicklace is, if not the default.

What you do not build

The parts of this that would otherwise be routes, tables and jobs in the same repository.

  • A subscribe endpoint. One public address takes a signup from a static export or a client component: idempotent on the address and the list, a honeypot, rate limited, CORS for the origins you allow, and an answer in under a second.
  • A users table with a lifecycle column. Stages come from the events as they arrive — Visitor, Subscriber, Account, Activated, Purchaser, Repeat — and nobody sets one by hand.
  • A mailing tool with its own copy of the same people. Lists live on the record, with a one-click unsubscribe in every email and the consent sentence stored beside each subscription.
  • A cron job for the follow-up. An automation is a sentence — When · Wait · Only if · Then — and Kicklace's own worker runs it.

Questions

Where does the tag go?
Inside the body of your root layout, as one component from kicklace/next. It reads the two public environment variables, so it takes no props, and all it renders is a script element — which is why it needs nothing from the router and works the same wherever you put it.
Do I need a Stripe route handler?
Usually not. Kicklace takes Stripe's webhook itself, and reads refunds, trials, renewals and cancellations as well as the payment. Write your own only if the payment already does work of yours, and then use one path or the other — running both lands the purchase twice unless they agree on the idempotency key.
Does this work with a static export, or on the edge?
Yes to both. The tag is a script and needs no server at all, and a signup from a static site can go to the public address, which is idempotent, rate limited and answers in under a second. The package imports nothing from node:, so it runs on edge runtimes, in workers, in Bun and in Deno.
What is anonId, and do I have to send it?
It is the random id the tag keeps in the browser's own storage. Sending it with a sign-up or a subscribe is what joins the anonymous visitor who read your pricing page last week to the account created today — one record, with the earliest first touch kept. Without it they are two records until an email address joins them.
Which environment variables do I need?
The public key for the tag and a secret key for your server, plus the two address variables if your Kicklace is not at the default. The secret key belongs on the server only: handed one, the tag renders nothing and says why in the console, because a component's output is in the page's source.

Read next

Join the waitlist

We are onboarding one company at a time. Leave your email and we will let you know when your workspace is ready.

Optional.

Optional. Tell us about your product and where your leads come from.

Which plan would you want?

Prices are not final and nothing is charged. Pick a plan now and you will get that price when billing opens.

By joining, you agree to receive waitlist updates from Kicklace: Put me on the Kicklace waitlist and tell me when a workspace can open for my company. We store this wording with the date and your email address. Every email has a one-click unsubscribe link. Privacy policy.

We reply to every signup.