Overview
The Node.js SDK is designed for server-side tracking where users are already authenticated. Unlike browser tracking, server events:
- Require identity - Email or user ID must be provided on every call
- Skip anonymous phase - Events go directly to the customer profile
- Support batching - Events are queued and sent efficiently
Use server-side tracking for events that happen in your backend: subscription changes, feature usage from APIs, background jobs, etc.
Installation
Quick Start
import { Outlit } from '@outlit/node'
const outlit = new Outlit({
publicKey: 'pk_your_public_key'
})
// Track an event
outlit.track({
email: 'user@example.com',
eventName: 'subscription_upgraded',
properties: {
fromPlan: 'starter',
toPlan: 'pro',
mrr: 99
}
})
// Important: flush before process exits
await outlit.flush()
Configuration
const outlit = new Outlit({
publicKey: 'pk_xxx',
apiHost: 'https://app.outlit.ai', // optional
flushInterval: 10000, // ms, default 10s
maxBatchSize: 100, // events per batch
timeout: 10000, // request timeout ms
})
Options
Your organization’s public key.
apiHost
string
default:"https://app.outlit.ai"
API endpoint for sending events.
How often to automatically flush events (milliseconds).
Maximum events per batch before auto-flush.
HTTP request timeout in milliseconds.
API Reference
outlit.track(options)
Track a custom event for an identified user.
outlit.track({
email: 'jane@acme.com',
userId: 'usr_12345', // optional if email provided
eventName: 'report_exported',
properties: {
reportType: 'sales',
format: 'pdf',
rowCount: 1500
},
timestamp: Date.now() // optional, defaults to now
})
User’s email address. At least one of email or userId is required.
Your internal user ID. At least one of email or userId is required.
Name of the event. Use snake_case.
Additional data to attach to the event.
Unix timestamp in milliseconds. Defaults to current time.
Unlike browser tracking, identity is required on every track() call. The SDK throws an error if neither email nor userId is provided.
outlit.identify(options)
Update user traits without tracking an event.
outlit.identify({
email: 'jane@acme.com',
userId: 'usr_12345',
traits: {
name: 'Jane Doe',
company: 'Acme Inc',
plan: 'enterprise',
role: 'admin',
lastActiveAt: new Date().toISOString()
}
})
Properties to update on the user profile.
Journey Stage Methods
Track user progression through your product lifecycle. All stage methods require either email or userId.
outlit.activate(options)
Mark a user as activated. Typically called after a user completes onboarding or a key activation milestone.
outlit.activate({
email: 'user@example.com',
properties: { flow: 'onboarding' }
})
outlit.engaged(options)
Mark a user as engaged. Typically called when a user reaches a usage milestone. Can also be computed automatically by the engagement cron.
outlit.engaged({
userId: 'usr_123',
properties: { milestone: 'first_project_created' }
})
outlit.paid(options)
Mark a user as paid. Typically called after a successful payment or subscription.
outlit.paid({
email: 'user@example.com',
properties: { plan: 'pro', amount: 99 }
})
outlit.churned(options)
Mark a user as churned. Typically called when a subscription is cancelled.
outlit.churned({
email: 'user@example.com',
properties: { reason: 'cancelled_by_user' }
})
The discovered and signed_up stages are automatically inferred from identify calls. You only need to explicitly call activate(), engaged(), paid(), or churned().
outlit.flush()
Immediately send all queued events. Call this before your process exits.
outlit.shutdown()
Gracefully shutdown: flushes remaining events and stops the timer.
outlit.queueSize
Get the number of events waiting to be sent.
console.log(`Pending events: ${outlit.queueSize}`)
Framework Examples
Express.js
import express from 'express'
import { Outlit } from '@outlit/node'
const app = express()
const outlit = new Outlit({ publicKey: 'pk_xxx' })
// Middleware to attach tracker to request
app.use((req, res, next) => {
req.outlit = outlit
next()
})
app.post('/api/reports/export', async (req, res) => {
const { user, format } = req.body
// Your export logic
const report = await generateReport(format)
// Track the event
req.outlit.track({
email: user.email,
eventName: 'report_exported',
properties: { format, size: report.size }
})
res.json({ success: true })
})
// Graceful shutdown
process.on('SIGTERM', async () => {
await outlit.shutdown()
process.exit(0)
})
Next.js API Routes
// app/api/subscription/upgrade/route.ts
import { Outlit } from '@outlit/node'
const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! })
export async function POST(request: Request) {
const { userId, email, newPlan, amount } = await request.json()
// Your upgrade logic
await upgradeSubscription(userId, newPlan)
// Track the event
outlit.track({
email,
userId,
eventName: 'subscription_upgraded',
properties: { plan: newPlan }
})
// Mark as paid
outlit.paid({
email,
properties: { plan: newPlan, amount }
})
// Flush immediately (serverless)
await outlit.flush()
return Response.json({ success: true })
}
Serverless Functions (AWS Lambda, Vercel)
In serverless environments, always call flush() before returning. The function may be terminated before the background flush runs.
import { Outlit } from '@outlit/node'
const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! })
export async function handler(event) {
const { email, action } = JSON.parse(event.body)
outlit.track({
email,
eventName: action,
properties: { source: 'lambda' }
})
// Critical: flush before returning
await outlit.flush()
return { statusCode: 200, body: 'OK' }
}
Stripe Webhooks
Track payment and subscription events from Stripe:
import { Outlit } from '@outlit/node'
import Stripe from 'stripe'
const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! })
export async function handleStripeWebhook(event: Stripe.Event) {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
if (session.customer_email) {
outlit.paid({
email: session.customer_email,
properties: {
amount: session.amount_total ? session.amount_total / 100 : undefined,
currency: session.currency,
}
})
}
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
// Get customer email from your database
const customerEmail = await getCustomerEmail(subscription.customer)
if (customerEmail) {
outlit.churned({
email: customerEmail,
properties: {
reason: subscription.cancellation_details?.reason,
}
})
}
break
}
}
await outlit.flush()
}
Background Jobs (Bull, Agenda)
import { Queue, Worker } from 'bullmq'
import { Outlit } from '@outlit/node'
const outlit = new Outlit({ publicKey: 'pk_xxx' })
const worker = new Worker('emails', async (job) => {
const { email, templateId } = job.data
// Your job logic
await sendEmail(email, templateId)
// Track completion
outlit.track({
email,
eventName: 'email_sent',
properties: { templateId, jobId: job.id }
})
})
// Periodic flush (events batch automatically)
// No need to flush per job unless critical
Common Events to Track
Here are recommended server-side events:
| Event | When | Properties |
|---|
subscription_created | New subscription | plan, price, interval |
subscription_upgraded | Plan upgrade | fromPlan, toPlan, mrr |
subscription_cancelled | Cancellation | reason, mrr_lost |
payment_succeeded | Payment received | amount, currency |
payment_failed | Payment failed | amount, error_code |
feature_used | Feature usage | feature, count |
api_key_created | API key created | keyName, permissions |
export_completed | Data export | format, rowCount, size |
invite_sent | Team invite | inviteeEmail, role |
user_role_changed | Permission change | fromRole, toRole |
Error Handling
The SDK silently handles errors to not disrupt your application. For debugging:
const outlit = new Outlit({ publicKey: 'pk_xxx' })
// Events are queued even if the network is down
outlit.track({ email: 'user@test.com', eventName: 'test' })
// Check queue size to detect potential issues
if (outlit.queueSize > 500) {
console.warn('Outlit queue backing up - possible connectivity issue')
}
TypeScript Support
Full TypeScript support is included:
import {
Outlit,
type OutlitOptions,
type ServerTrackOptions,
type ServerIdentifyOptions,
type StageOptions,
} from '@outlit/node'
const options: OutlitOptions = {
publicKey: 'pk_xxx',
flushInterval: 5000,
}
const outlit = new Outlit(options)
const trackOptions: ServerTrackOptions = {
email: 'user@test.com',
eventName: 'test',
properties: { key: 'value' }
}
outlit.track(trackOptions)
// Stage options
const stageOptions: StageOptions = {
email: 'user@test.com',
properties: { plan: 'pro' }
}
outlit.paid(stageOptions)
Next Steps