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.
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.
import { Kicklace } from "kicklace";
const kl = new Kicklace(process.env.KICKLACE_SECRET_KEY!);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.
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.