Add it to anything. One snippet, or one endpoint.
Every stack and every level of complexity, with exact copy-paste code: a website, React, Vue, Next.js, a multi-tenant SaaS, iOS and Android, any backend language, no-code builders, and self-host. Then connect your AI and ask your numbers in plain english.
There is one endpoint. Everything else is a convenience on top.
Getting data in is deliberately small. In a browser, a script gives you autocapture and helpers. Everywhere else, you send events with one HTTP call, no dependency to add or keep updated:
POST https://YOUR_HOST/v1/events
Authorization: Bearer YOUR_WRITE_KEY
Content-Type: application/json
{ "name": "checkout", "distinct_id": "user_123", "properties": { "amount": 29, "plan": "pro" } }That is the entire API for sending data. A single event or an array of up to 10,000. The write key is write-only, so it is safe in client code. Keep the distinct_id stable and identical across web and server, and a person's events join into one funnel on their own.
A website or web app
Paste this into your <head>. It captures pageviews and clicks immediately; add track() for the moments you want funnels on.
<script src="https://YOUR_HOST/sdk.js"></script>
<script>
smolanalytics.init("YOUR_WRITE_KEY", { host: "https://YOUR_HOST" });
// the moments that matter (optional, but this is what powers funnels)
smolanalytics.identify("user_123"); // ties a person's events together
smolanalytics.track("signup", { plan: "pro" });
smolanalytics.track("activate"); // your core aha moment
</script>React or Vue
Same script, loaded once from your entry file so it is bundled with the app. SPA route changes (React Router, TanStack, Vue Router) are captured automatically, no per-route wiring.
export function initAnalytics() {
if (document.getElementById("smol")) return;
const s = document.createElement("script");
s.id = "smol"; s.src = "https://YOUR_HOST/sdk.js";
s.onload = () => (window as any).smolanalytics.init("YOUR_WRITE_KEY", { host: "https://YOUR_HOST" });
document.head.appendChild(s);
}
// call initAnalytics() once in src/main.tsx, before renderexport default defineNuxtConfig({
app: { head: { script: [
{ src: "https://YOUR_HOST/sdk.js" },
{ children: `smolanalytics.init("YOUR_WRITE_KEY", { host: "https://YOUR_HOST" });` },
] } },
});Next.js (App Router or Pages)
Load it with next/script in your root layout. Pageviews and clicks are captured on every route.
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<Script src="https://YOUR_HOST/sdk.js" strategy="afterInteractive" />
<Script id="smol-init" strategy="afterInteractive">
{`smolanalytics.init("YOUR_WRITE_KEY", { host: "https://YOUR_HOST" });`}
</Script>
</html>
);
}Server-side events (a Route Handler, a Server Action, a Stripe webhook) post straight to the endpoint with the same distinct_id you pass to identify():
await fetch(`${process.env.SMOLANALYTICS_HOST}/v1/events`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SMOLANALYTICS_KEY}` },
body: JSON.stringify({ name: "checkout", distinct_id: userId, properties: { amount: 29 } }),
});A multi-tenant SaaS
A real SaaS emits events from two places, and smolanalytics is built for exactly that split. The browser sends product usage; your server sends the things the browser never sees (payments, provisioning, webhooks, cron). Three rules make it clean:
- 1One identity everywhere. Call identify(userId) in the browser and send that same distinct_id from the server. Client and server events fuse into one funnel per user.
- 2Sites are never the meter. Every event is stamped with its site by the SDK, so all your surfaces (marketing site, app, docs) live on one instance and one bill. You are billed on events, not on how many sites or how many users you have.
- 3Hard isolation when you need it. Each project is its own isolated instance (its own server and data). Keep a big customer's data fully separate by giving them their own instance; Solo includes 2, Pro 5.
// Stripe webhook, billing cron, or provisioning job, any backend language.
await fetch(`${SMOL_HOST}/v1/events`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${SMOL_KEY}` },
body: JSON.stringify({
name: "subscription_started",
distinct_id: user.id, // the SAME id you identify() in the app
properties: { plan: "pro", mrr: 29, seats: 3 },
}),
});Now the funnel from a marketing pageview, to signup in the app, to a payment on your server is one connected path, and you ask it directly: "what's the signup to paid conversion, and how long does it take?"
Mobile apps (iOS, Android, React Native)
No SDK. Send events with the HTTP client the app already has. Use a stable distinct_id per user (your user id, or an id you store in Keychain / SharedPreferences), and track screens as events to get screen-flow paths.
func track(_ name: String, _ props: [String: Any] = [:], distinctId: String) {
var req = URLRequest(url: URL(string: "\(host)/v1/events")!)
req.httpMethod = "POST"
req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try? JSONSerialization.data(withJSONObject: [
"name": name, "distinct_id": distinctId, "properties": props,
])
URLSession.shared.dataTask(with: req).resume()
}
// track("signup", ["plan": "pro"], distinctId: userId)
// track("screen", ["name": "Checkout"], distinctId: userId) // screen-flow pathsfun track(name: String, props: Map<String, Any> = emptyMap(), distinctId: String) {
val payload = JSONObject(mapOf("name" to name, "distinct_id" to distinctId, "properties" to JSONObject(props)))
val body = payload.toString().toRequestBody("application/json".toMediaType())
val req = Request.Builder().url("$host/v1/events")
.addHeader("Authorization", "Bearer $key").post(body).build()
client.newCall(req).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, r: Response) { r.close() }
})
}export const track = (name: string, props = {}, distinctId: string) =>
fetch(`${HOST}/v1/events`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${KEY}` },
body: JSON.stringify({ name, distinct_id: distinctId, properties: props }),
});Battery tip: queue events and POST them as one array (up to 10,000) on background or foreground instead of one request each. Flutter and any other toolkit work the same way, it is the one endpoint with that toolkit's HTTP client.
Any backend language
The events the browser never sees (payments, webhooks, cron jobs, API usage) post from any language. Same distinct_id as the client so they join up.
await fetch(`${process.env.SMOL_HOST}/v1/events`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SMOL_KEY}` },
body: JSON.stringify({ name: "checkout", distinct_id: userId, properties: { amount: 29 } }),
});import requests
requests.post(f"{HOST}/v1/events",
headers={"Authorization": f"Bearer {KEY}"},
json={"name": "signup", "distinct_id": user_id, "properties": {"plan": "pro"}})body, _ := json.Marshal(map[string]any{"name": "checkout", "distinct_id": userID, "properties": map[string]any{"amount": 29}})
req, _ := http.NewRequest("POST", host+"/v1/events", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)require "net/http"; require "json"
uri = URI("#{HOST}/v1/events")
Net::HTTP.post(uri, { name: "signup", distinct_id: user_id }.to_json,
"Authorization" => "Bearer #{KEY}", "Content-Type" => "application/json")$ch = curl_init("$host/v1/events");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["name" => "checkout", "distinct_id" => $userId]),
]);
curl_exec($ch);Built with an AI builder
If you built your app with Lovable, Bolt, v0, Replit, or Base44 and you do not write code, you never open an editor. Sign up, and we hand you one prompt with your write key already in it. Paste it into your builder's chat and its AI drops the snippet in the right place for that platform.
Self-host the binary
The whole product is one open-source Go binary. Run it yourself, unlimited everything, same features, no account. The hosted cloud exists only for the day you would rather not run a server.
docker run -p 8080:8080 ghcr.io/arjun0606/smolanalytics # dashboard on http://localhost:8080, ingestion at /v1/events
Prefer to kick the tyres with real data first? The live demo is a populated instance, no install. Full self-host notes are in the GitHub README.
Connect your AI and just ask
Once events are flowing, run one command. It wires smolanalytics into every coding assistant you have, so your editor's own model answers from your real data, your model, so the AI part costs nothing.
smolanalytics connect # wires up Cursor, Claude Code, VS Code, Windsurf, …
Then ask, in the same window you write code:
Your assistant admits it hallucinates your numbers. This one cannot: answers come from the same deterministic reports the dashboard renders (47 MCP tools, 13 prompts), and a CI test fails the build if the editor and the dashboard ever disagree. There is also a dashboard ask bar for plain-english questions about your data. Which surface is which is explained on how it works.
The event contract
| field | required | what it is |
|---|---|---|
| name | yes | The event, e.g. signup, checkout. A $ prefix marks internal web events (pageviews); yours have no prefix. |
| distinct_id | no | Who did it. Use one stable value across web and server so a person's events join. Omit for anonymous counts. |
| properties | no | Any JSON object: plan, amount, source. What you break funnels and cohorts down by. |
- Auth: Authorization: Bearer YOUR_WRITE_KEY. Write-only, safe in client code.
- Batch: POST an array of up to 10,000 events in one request (max 4MB). Over the cap returns a clean 413.
- Endpoint: POST /v1/events on your instance host. That is the entire write API.
- Full API + all 47 tools: see every feature and the API doc.