Tutorial

How to Add CRM to Your Next.js App in 10 Minutes

March 15, 20269 min read

You've shipped a Next.js app with users signing up, but you have no organized view of who they are. Every time you want to know "how many paid users do I have?" or "who signed up this week?" you're querying Supabase or Postgres directly. This tutorial shows you how to get a proper customer dashboard in about 10 minutes.

We'll use the App Router pattern throughout. If you're on Pages Router, the concepts are identical — just adapt the route handler syntax.

Prerequisites

  • Next.js 14+ with App Router
  • An existing auth flow (Supabase Auth, NextAuth, Clerk, etc.)
  • A TinyCRM account (free trial at app.tinycrm.dev)
  • Node.js 18+

Step 1: Install the SDK

pnpm add tinycrm-sdk
# or npm install tinycrm-sdk

Add your API key to .env.local. Get it from your TinyCRM dashboard under Project Settings:

# .env.local
TINYCRM_API_KEY=tcrm_proj_xxxxxxxxxxxxxxxx

Step 2: Create a singleton client

Create a shared client module to avoid instantiating the SDK on every request:

// lib/tinycrm.ts
import { TinyCRM } from 'tinycrm-sdk';

if (!process.env.TINYCRM_API_KEY) {
  throw new Error('TINYCRM_API_KEY is not set');
}

export const tinycrm = new TinyCRM({
  apiKey: process.env.TINYCRM_API_KEY,
  silent: process.env.NODE_ENV === 'production',
});

The silent: false in development means you'll see console errors if something goes wrong during setup. In production, silent: true ensures TinyCRM never crashes your app.

Step 3: Identify users on signup

Find your signup route handler — wherever you create a new user. Add a tinycrm.identify() call right after user creation:

// app/api/auth/signup/route.ts
import { tinycrm } from '@/lib/tinycrm';

export async function POST(request: Request) {
  const { email, name, password } = await request.json();

  // Your existing auth logic
  const { data: user, error } = await supabase.auth.signUp({
    email,
    password,
    options: { data: { name } },
  });

  if (error || !user.user) {
    return Response.json({ error: error?.message }, { status: 400 });
  }

  // Sync to TinyCRM
  await tinycrm.identify({
    email,
    name,
    status: 'free',
    params: {
      source: request.headers.get('referer') ?? 'direct',
    },
  });

  return Response.json({ ok: true });
}

If you're using NextAuth or Clerk, add the identify call in their respective callback/event hooks:

// With NextAuth — auth.ts
import { tinycrm } from '@/lib/tinycrm';

export const { handlers, signIn, signOut, auth } = NextAuth({
  callbacks: {
    async signIn({ user }) {
      if (user.email) {
        await tinycrm.identify({
          email: user.email,
          name: user.name ?? undefined,
          status: 'free',
        });
      }
      return true;
    },
  },
});

Step 4: Track activity in middleware

Add a ping call to your middleware to keep last_activity_at current. Use fire-and-forget to avoid adding latency:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/auth'; // or your session method
import { tinycrm } from '@/lib/tinycrm';

export default auth(async (req) => {
  const session = req.auth;

  // Only ping for authenticated users on non-API routes
  if (session?.user?.email && !req.nextUrl.pathname.startsWith('/api')) {
    // Fire-and-forget — never await in middleware
    tinycrm.ping(session.user.email).catch(() => {});
  }

  return NextResponse.next();
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

Step 5: Update status on payment

When a user upgrades, identify them again with the new status. The call is idempotent — it updates what you pass, leaves everything else unchanged:

// app/api/webhooks/paddle/route.ts
import { tinycrm } from '@/lib/tinycrm';

export async function POST(request: Request) {
  const body = await request.json();

  if (body.alert_name === 'subscription_created') {
    await tinycrm.identify({
      email: body.email,
      status: 'paid',
      params: {
        plan: body.subscription_plan_id,
        paddle_subscription_id: body.subscription_id,
      },
    });
  }

  if (body.alert_name === 'subscription_cancelled') {
    await tinycrm.identify({
      email: body.email,
      status: 'free',
      params: { plan: 'cancelled' },
    });
  }

  return Response.json({ ok: true });
}

What you've built

With these three integrations — signup, middleware, and payment webhook — you have:

  • Every new signup automatically appears in your TinyCRM dashboard
  • Active users have up-to-date last_activity_at timestamps
  • Payment status is automatically synced from your payment provider
  • Custom params (plan, source) are stored for filtering

In your TinyCRM dashboard, you can now filter by status, sort by activity, and see which plan each customer is on. Click any customer for their full profile.

TypeScript types

The SDK ships with full TypeScript types. Here's the identify payload type for reference:

interface IdentifyPayload {
  email: string;                              // required
  name?: string;
  status?: 'free' | 'paid';
  params?: Record<string, string | number | boolean>;
}

// Usage
await tinycrm.identify({
  email: 'user@example.com',
  name: 'Jane Doe',
  status: 'paid',
  params: { plan: 'pro', cohort: 'march-2026' },
});

Ready to integrate?

Start your free trial, create a project, and copy your API key. The whole integration takes about 10 minutes.