Modules
Docs

Your sales cycle,
inside your own app.

An open source CRM engine for leads, agents, sales activity, partner commissions, and disputes. Add it to the Node app and Postgres database you already run. Your data stays in your database.

// ONE SALES CYCLE, END TO END

From first lead
to a clean payout record.

▲ Capture a lead. Route it to the right agent. Keep the sales trail. Hold and release commission. Resolve disputes with a record you can replay. Tandem gives your app the workflow primitives; you keep the decisions.

Hand-drawn canyon expedition scene with a nest, hikers, trail fork, river, and distant summit
// RUNS ON YOUR POSTGRES, WHEREVER IT LIVES
// WHAT IT DOES

A CRM foundation
for your app.

Tandem keeps a clear record in Postgres so your app can show the whole sales journey and your team can trace what happened.

01 / EVENT LOG

Append-only source of truth

Every important change is a typed event. You can trace the story without losing earlier decisions.

02 / PROJECTIONS

Rebuildable state

Lead, payout, and activity views build from events. Rebuild them whenever you need to.

03 / MONEY

Integer minor units

Commission amounts use whole cents or sen. Rounding is part of the rule, so the numbers stay clear.

04 / RULES

Qualification & routing

Set a qualification threshold. Choose round robin, least loaded, or manual assignment for each lead.

05 / ESCROW

Escrow, ready to schedule

Hold commissions until they are ready. Run release_due_commissions() on the schedule that suits your app.

06 / AUTH

Vendor-neutral RLS

Use Supabase, plain Postgres, or your own identity provider. Tandem keeps each workspace separate.

// HOW IT WORKS

Start with your app
and Postgres.

Tandem fits into the routes and database you already use.

app/api/webhooks/route.ts
// Minimal install: engine + Postgres. Nothing else.
import { createTandemPool, applyTandemMigrations, withTandemSession } from 'tandem-crm/db';
import { qualifyLead, defaultTandemConfig } from 'tandem-crm';

const pool = createTandemPool(process.env.DATABASE_URL);
await applyTandemMigrations(pool);

// Adapters map external payloads into typed events, then write them
// inside a transaction scoped to the caller's identity.
await withTandemSession(pool, userId, async (client) => {
  const { status } = qualifyLead({ qualificationMetric: 12 }, defaultTandemConfig);
  await client.query('insert into tandem.events (...) values (...)', [/* ... */]);
});

// release_due_commissions() runs inside Postgres. Wire it to pg_cron
// or any scheduler you already have. No separate worker process.
// ROADMAP

The customer journey,
kept together.

A customer's journey from first lead to a clear, lasting record.

Live

Terrain

The ground truth beneath the CRM: leads, typed event history, rebuildable state, exact money math, and tenant-safe Postgres access.

Live

Ascent

Agent onboarding and certification. Teams define the checkpoints; agents complete them; your app decides what certification unlocks.

Live

Camp

A full CRM interface for your team: leads, agents, sales activity, commissions, disputes. It mounts into your own Next.js app in one catch-all route.

Live

Trail

Lightweight sales activity for a real CRM: calls, emails, visits, confidence, stages, corrections, and retractions without deleting history. Log CHAMP qualification when you want it: challenges, authority, money, prioritization.

Live

Waypoint

Territory-aware recommendations for who should own a lead: round-robin, least-loaded, or manual. Terrain records what is true; Waypoint recommends where to go next.

Live

Belay

A safe place to handle commission disputes. It holds a payout while a case is open and records the operator's decision.

// READ THE CODE

One runtime dependency,
small enough to read in an afternoon.

It's MIT licensed, so you can self-host it, fork it, swap out the UI, or strip out whatever you don't need.

View on GitHub Read the docs