Express SDK
Add Perly churn prevention to your Express.js application with simple middleware and a user resolver function.
Installation
npm install @perly/express @perly/coreUsage
Create a Perly client with createPerlyClient and register the middleware with perlyMiddleware. The middleware intercepts every request, resolves the current user via your userResolver, and attaches a perly client to the request object.
// server.ts
import express from 'express';
import { createPerlyClient, perlyMiddleware, PerlyBuilder } from '@perly/express';
const perly = createPerlyClient({
apiKey: process.env.PERLY_API_KEY!,
userResolver: (req) => {
const user = req.user;
if (!user) return null;
return new PerlyBuilder()
.setId(user.id)
.setMetadata({ plan: user.plan, region: user.region })
.linkStripeById(user.stripeCustomerId)
.linkHubspotById(user.hubspotContactId)
.build();
},
});
const app = express();
// middleware to authenticate users and other prehandle filters,
// so that perly only gets the requests it should
app.use(perlyMiddleware(perly));
app.listen(3000);User Resolver
The userResolver is a function passed to createPerlyClient. It receives the Express Request object and returns a Perly user built with PerlyBuilder, or null to skip tracking for unauthenticated requests.
import { PerlyBuilder, PerlyUser } from '@perly/core';
import { Request } from 'express';
function resolvePerlyUser(req: Request): PerlyUser | null {
const user = req.user;
if (!user) return null;
return new PerlyBuilder()
.setId(user.id)
.setMetadata({
plan: user.plan,
region: user.region,
companyId: user.companyId,
})
.linkStripeById(user.stripeCustomerId)
.linkHubspotById(user.hubspotContactId)
.build();
}Tracking Events
Use req.perly inside any route handler to track events that reflect customer engagement and health.
app.post('/onboarding/complete', async (req, res) => {
await req.perly.track('onboarding_completed');
res.json({ success: true });
});
app.post('/reports/export', async (req, res) => {
await req.perly.track('report_exported', {
format: req.body.format,
});
res.json({ url: reportUrl });
});
app.post('/team/invite', async (req, res) => {
await req.perly.track('team_member_invited', {
email: req.body.email,
});
res.json({ invited: true });
});Expansion Signals
Send signals when customers approach usage thresholds. These feed into Perly expansion workflows and upsell triggers.
app.use('/api', async (req, res, next) => {
const usage = await getApiUsage(req.user.id);
if (usage.current > usage.limit * 0.9) {
await req.perly.signal('api_usage_high', {
current: usage.current,
limit: usage.limit,
period: 'monthly',
});
}
next();
});
// Other signal types
await req.perly.signal('seat_limit_near', { current: 48, limit: 50 });
await req.perly.signal('storage_limit_near', { usedGb: 9.2, limitGb: 10 });
await req.perly.signal('rate_limit_hit', { endpoint: '/api/search' });