Express SDK

Add Perly churn prevention to your Express.js application with simple middleware and a user resolver function.

@perly/expressv1.0.03 minutes

Installation

npm install @perly/express @perly/core

Usage

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' });