Developers
Everything you need to add Luchy to an app. The package is luchy on npm and the source is on GitHub.
1. Add the script
Any site, any framework. Pageviews start flowing immediately.
<script
src="https://cdn.luchy.app/luchy.min.js"
data-api-key="YOUR_INGEST_KEY"
data-auto-pageviews
defer
></script>2. Install the package
Browser, server and React Router entry points ship in one package.
bun add luchy3. React Router middleware
Return the signed-in user, their tenant and, when someone is impersonating, the real actor. Return undefined for anonymous traffic. Identity is signed once per request and shared with the browser script, so both land in one session.
// app/root.tsx
import { env } from 'cloudflare:workers';
import { luchyMiddleware, getLuchy, LuchyScript } from 'luchy/react-router';
export const middleware = [
luchyMiddleware({
apiKey: env.LUCHY_API_KEY,
secret: env.LUCHY_SECRET_KEY,
waitUntil: ({ context }) => (p) => context.cloudflare.ctx.waitUntil(p),
identity: async ({ request }) => {
const session = await getSession(request);
if (!session) return undefined;
return {
user: session.user.id,
tenant: String(session.workspaceId),
actor: session.impersonatorId,
props: { role: session.user.role },
traits: { name: session.user.name, email: session.user.email }
};
}
})
];
export const loader = ({ context }: Route.LoaderArgs) => {
return { luchy: getLuchy(context) };
};
// in <Layout>, next to <Scripts />
<LuchyScript {...loaderData.luchy} />The middleware also turns every mutation into a named event, after the route and intent. IDs collapse to :id, reads are skipped, redirects still count, and events ship through waitUntil so responses never wait.
| Request | Event |
|---|---|
POST /settings/team intent=invite | settings/team:invite |
POST /invoices/1042/send 303 redirect | invoices/:id/send |
DELETE /projects/42 intent=archive | projects/:id:archive |
GET /reports read | skipped, a read |
4. Any other server
Send events from any server, worker or queue consumer with luchy/server. Same signed identity, same sessions.
import { createLuchy } from 'luchy/server';
const luchy = createLuchy({
apiKey: env.LUCHY_API_KEY,
secret: env.LUCHY_SECRET_KEY
});
await luchy.track(
{ name: 'export:csv', pathname: '/reports', payload: { rows: 1200 } },
{ user: 'u_1042', tenant: 'acme' }
);Read-only SQL
Your events and sessions live in plain tables. Query them directly with read-only SQL.
-- actions per user per tenant, last 30 days
select s.user, e.tenant, count(*) as actions
from events e
join sessions s on s.id = e.session_id
where e.type = 'event'
and e.created_at > datetime('now', '-30 days')
group by s.user, e.tenant
order by actions desc
limit 20;Ask from Claude (MCP)
Luchy is an MCP server at https://dash.luchy.app/mcp. Mint an API key in the dashboard under Keys and send it as Authorization: Bearer wak_…. Ask “who used my app this week vs last?” and the answer comes back with numbers and, in hosts that support MCP Apps, a chart.
{
"mcpServers": {
"luchy": {
"command": "npx",
"args": [
"mcp-remote",
"https://dash.luchy.app/mcp",
"--header",
"Authorization:Bearer ${LUCHY_KEY}"
],
"env": { "LUCHY_KEY": "wak_…" }
}
}
}The same methods are a plain HTTP API: POST /api/analytics.query, documented at /llms.txt and /openapi.json.