Skip to main content

Guides

Express.js Integration

This guide shows how to integrate RecurCite with an Express.js application. You'll add evidence tracking to your authentication, billing, and feature usage routes.

Prerequisites

  • Express.js 4.x or 5.x
  • Node.js 18+
  • @recurcite/sdk installed
  • A RecurCite API key

1. Initialize the SDK

lib/recurcite.ts
import { init } from "@recurcite/sdk";

export const recurcite = init({
  apiKey: process.env.RECURCITE_API_KEY!,
  signingSecret: process.env.RECURCITE_SIGNING_SECRET,
});

2. Track logins

Add login tracking to your authentication route. Express gives you direct access to the request IP and user agent:

routes/auth.ts
import { Router } from "express";
import { recurcite } from "../lib/recurcite";

const router = Router();

router.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  if (!user) {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  // Track login for RecurCite evidence
  await recurcite.track({
    type: "user.login",
    payload: {
      occurred_at: new Date().toISOString(),
      ip: req.ip,
      user_agent: req.headers["user-agent"],
    },
    stripe_refs: {
      stripe_customer_id: user.stripeCustomerId,
    },
  });

  const token = generateToken(user);
  res.json({ token });
});

export default router;

3. Track terms acceptance

routes/terms.ts
router.post("/accept-terms", requireAuth, async (req, res) => {
  const { termsVersion } = req.body;

  await recurcite.track({
    type: "terms.accepted",
    payload: {
      version: termsVersion,
      accepted_at: new Date().toISOString(),
    },
    stripe_refs: {
      stripe_customer_id: req.user.stripeCustomerId,
    },
  });

  res.json({ success: true });
});

4. Usage tracking middleware

For automatic feature usage tracking, create an Express middleware that fires on specific routes:

middleware/track-usage.ts
import { recurcite } from "../lib/recurcite";

/**
 * Express middleware to track feature usage.
 * Use on routes that represent billable or trackable actions.
 */
export function trackUsage(featureKey: string) {
  return async (req: any, _res: any, next: any) => {
    if (req.user?.stripeCustomerId) {
      // Fire and forget — don't block the request
      recurcite.track({
        type: "product.used",
        payload: {
          feature_key: featureKey,
          count: 1,
          occurred_at: new Date().toISOString(),
        },
        stripe_refs: {
          stripe_customer_id: req.user.stripeCustomerId,
        },
      }).catch(() => {}); // Non-blocking
    }
    next();
  };
}

// Usage:
// app.post("/api/exports", trackUsage("exports"), exportHandler);
// app.get("/api/reports", trackUsage("reports"), reportHandler);

Tip

The fire-and-forget pattern (.catch(() => {})) ensures evidence tracking never blocks or breaks your request flow. The SDK handles retries internally.

5. Track cancellations

routes/billing.ts
router.post("/cancel", requireAuth, async (req, res) => {
  const { subscriptionId } = req.body;

  // 1. Track cancellation request
  await recurcite.track({
    type: "cancellation.requested",
    payload: { occurred_at: new Date().toISOString() },
    stripe_refs: {
      stripe_customer_id: req.user.stripeCustomerId,
      stripe_subscription_id: subscriptionId,
    },
  });

  // 2. Cancel in Stripe
  const sub = await stripe.subscriptions.cancel(subscriptionId);

  // 3. Track confirmed
  await recurcite.track({
    type: "cancellation.confirmed",
    payload: {
      occurred_at: new Date().toISOString(),
      receipt_id: sub.id,
    },
    stripe_refs: {
      stripe_customer_id: req.user.stripeCustomerId,
      stripe_subscription_id: subscriptionId,
    },
  });

  res.json({ success: true, canceledAt: sub.canceled_at });
});

6. Stripe webhook for CE3

Track transaction.completed events in your Stripe webhook to enable CE3:

routes/stripe-webhook.ts
import { Router } from "express";
import Stripe from "stripe";
import { recurcite } from "../lib/recurcite";

const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

router.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const event = stripe.webhooks.constructEvent(
      req.body,
      req.headers["stripe-signature"]!,
      process.env.STRIPE_BILLING_WEBHOOK_SECRET!
    );

    if (event.type === "invoice.paid" || event.type === "invoice.payment_succeeded") {
      const invoice = event.data.object as Stripe.Invoice;

      await recurcite.track({
        type: "transaction.completed",
        payload: {
          charge_id: invoice.charge as string,
          amount: invoice.amount_paid,
          currency: invoice.currency,
          occurred_at: new Date(invoice.created * 1000).toISOString(),
        },
        stripe_refs: {
          stripe_customer_id: invoice.customer as string,
          stripe_subscription_id: invoice.subscription as string,
          stripe_invoice_id: invoice.id,
        },
      });
    }

    res.json({ received: true });
  }
);

export default router;

Environment variables

.env
RECURCITE_API_KEY=rc_live_your_api_key_here
RECURCITE_SIGNING_SECRET=your_signing_secret_here

Next steps