Tutorial

Building a CRM with the TinyCRM SDK: Step-by-Step

March 15, 202612 min read

You've got users signing up for your product, but you have no organized view of who they are, what plan they're on, or when they were last active. This tutorial walks through setting up TinyCRM from scratch — from creating your first project to having a populated, filterable customer table.

We'll cover everything: account setup, SDK installation, instrumenting your auth flow, activity tracking, and importing existing customers from CSV. By the end, you'll have a working customer database that updates automatically.

What you'll need

  • A TinyCRM account (free 14-day trial)
  • A Node.js application (Next.js, Express, or similar)
  • Node.js 18+ (for native fetch support)
  • About 20 minutes

Step 1: Create your account and project

Go to app.tinycrm.dev/signup and create an account. The 14-day free trial gives you full access to all features for up to 100 customers.

After signing up, navigate to Projects and create a project for each of your products. A project has:

  • A name (e.g., "My SaaS App")
  • A unique API key in the format tcrm_proj_xxxxxxxxxxxxxxxx

Copy the API key — you'll use it as an environment variable. Keep it secret; it's used in server-side code only.

Step 2: Install the SDK

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

Add your API key to your environment:

# .env.local
TINYCRM_API_KEY=tcrm_proj_xxxxxxxxxxxxxxxx

Create a singleton instance to reuse across your app:

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

export const tinycrm = new TinyCRM(
  process.env.TINYCRM_API_KEY!
);

Step 3: Instrument your signup flow

The most important call is identify(). Call it whenever a new user registers, or whenever you have updated information about an existing user.

For Next.js App Router:

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

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

  // Your existing user creation logic
  const user = await createUser({ email, name, password });

  // Sync to TinyCRM — runs in background, won't block response
  await tinycrm.identify({
    email: user.email,
    name: user.name,
    status: 'free',
    params: {
      source: body.utm_source ?? 'direct',
      plan: 'free',
    },
  });

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

The params object is where you store custom data. Pass anything that would be useful to see in your customer list later — plan, source, role, pricing tier, etc. Values are shallow-merged, so subsequent calls only update the keys you pass.

Step 4: Update status on payment events

When a customer upgrades from free to paid, call identify() again with the updated status. You can also update params with plan information from your payment provider.

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

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

  if (event.alert_name === 'subscription_created') {
    const email = event.email;
    const plan = event.subscription_plan_id;

    await tinycrm.identify({
      email,
      status: 'paid',
      params: {
        plan,
        paddle_subscription_id: event.subscription_id,
      },
    });
  }

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

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

Step 5: Track activity with ping()

ping() is a lightweight call that only updates last_activity_at. Call it on user login or from session middleware to keep activity timestamps current.

// middleware.ts
import { tinycrm } from '@/lib/tinycrm';

export async function middleware(request: NextRequest) {
  // Your existing session/auth middleware...
  const session = await getSession(request);

  if (session?.user?.email) {
    // Fire-and-forget — don't await in middleware
    tinycrm.ping(session.user.email).catch(() => {});
  }

  return NextResponse.next();
}

Note that we don't await in middleware — pinging TinyCRM should never block page loads. The SDK is silent by default, so if the call fails, nothing bad happens.

Step 6: Import existing customers from CSV

If you have existing customers in a spreadsheet or from another tool, you can import them via CSV. In the TinyCRM dashboard, go to Import and upload a CSV with at minimum an email column.

The column matcher lets you map your spreadsheet columns to TinyCRM fields:

  • Required: email
  • Optional: name, status
  • Any other columns become params key:value pairs

Existing customers are merged by email — no duplicates are created if a record already exists.

Step 7: Explore your customer dashboard

With data flowing in, your dashboard shows a filterable table of all customers. You can:

  • Filter by project (see customers for a specific product)
  • Filter by status (free vs paid)
  • Sort by signup date or last activity
  • Search by name or email
  • Click any customer to see their full profile and project history
  • Export filtered results as CSV

The customer profile page shows all projects the customer is associated with, their status per project, custom params, and activity timeline.

Common integration patterns

Pattern 1: Onboarding funnel tracking

// Track each step of onboarding in params
await tinycrm.identify({
  email: user.email,
  params: {
    onboarding_step: 'completed_profile',
    has_created_project: true,
    has_installed_sdk: false,
  },
});

Pattern 2: Feature flags / segments

// Store segment data in params for filtering later
await tinycrm.identify({
  email: user.email,
  params: {
    cohort: 'q1-2026',
    beta_features: true,
    source: 'product-hunt',
  },
});

Pattern 3: Multi-product customers

// Each product has its own TinyCRM instance with a different API key
// Same email = same customer row, multiple project badges

// In Product A:
const tinycrmA = new TinyCRM(process.env.TINYCRM_API_KEY_PRODUCT_A!);
await tinycrmA.identify({ email: user.email, status: 'paid' });

// In Product B:
const tinycrmB = new TinyCRM(process.env.TINYCRM_API_KEY_PRODUCT_B!);
await tinycrmB.identify({ email: user.email, status: 'free' });

// Result: one customer row, two project badges

Error handling and resilience

The SDK's default silent: true mode means TinyCRM never crashes your application. If the API is down, the call fails silently and your app continues normally. In development, set silent: false to see errors:

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

Ready to build?

Start your free 14-day trial. Full access, no credit card required. Cancel anytime.