The master lead list and suppression database for outbound agencies running multiple clients and campaigns. Every CSV you upload gets deduplicated against one shared contacts/companies database, so a lead that's already in another client's list, a past campaign, or a do-not-contact segment never slips through. Build campaign-specific segments with nested AND/OR filters, exclude suppression lists in one click, and share read-only views with clients — no install, no spreadsheet juggling.
- Email/password authentication — every team member logs in; the whole app is gated except public
/f/[slug]share links sent to clients. The first account created via/setupbecomes an admin automatically - Admin-managed permissions — admins manage the team at
/admin/users: create/delete accounts and control who can upload/edit data (canManageData), who can save and share segments (canManageViews), and who can export (canExport) — so junior team members can run campaigns without touching the master dataset - Master Contacts database — every uploaded list is matched against shared
contacts/companiestables by email/domain and deduped (fill-blanks-only merge). One person who appears across five client uploads becomes one record — the foundation for cross-client suppression - Master Contacts home view — the app opens on the union of every uploaded list, so you can filter, segment, and export across your entire book of leads from the start, not just one client's file
- Exclude lists/views — suppress an entire client's list, or every row matching a saved view's rules (e.g. "already replied", "do not contact"), from filtering, pagination, and exports in one toggle — the core suppression-list workflow for outbound
- Visual filter builder — nested AND/OR rule groups with per-column type inference (text, number, date, boolean) for building campaign segments (e.g. title contains "VP Sales" AND company size > 50 AND NOT in suppression list)
- Smart column types — automatically detects column data types and shows appropriate operators (contains, equals, >, <, is empty, etc.)
- Live preview — segment results update in real time as rules are modified (300ms debounce), so you see campaign list size before exporting
- Server-side pagination — results, lead lists, and the master database are fetched 25 rows at a time from the server, so filtering and browsing stay fast even with tens of thousands of leads across multiple clients
- Stats bar — shows match count vs total rows and active rule count at a glance — a quick sanity check on segment size before sending to a sequencer
- Saved Views — save named segment definitions per client or campaign (optionally password-protected) for reuse and re-sharing, backed by a Neon PostgreSQL database
- Shareable client links — turn any segment into a read-only
/f/[slug]link, optionally password-protected, capped to the first 500 matching rows and paginated 25 rows at a time, with a one-click CSV download. Share a target list with a client or VA without giving them access to the master database. If the underlying data isn't yet a saved Lead List, sharing auto-saves it first - URL title/description — set custom Open Graph title/description/image when creating a shared link, so it looks branded when sent to a client
- Lead Lists — organize uploads by client, campaign, or source; stored in the database. Large CSVs (20k+ rows) upload directly to Cloudflare R2 via presigned URLs, bypassing serverless request-size limits
- Upload provenance — when saving a lead list, add an optional note; every row gets a
Noteand anUpload Date(DD/MM/YYYY) column, so when a lead shows up in the master database you can trace which client list or campaign it came from - Lookup Wizard — guided workflow for enriching or matching filtered rows against external data (e.g. merge in verified emails before exporting a segment)
- Undo/redo — full filter state history with keyboard shortcut support
- Export — download matching rows as CSV
- Sample data — one-click load of a built-in sample dataset to explore features without uploading a file
- Dark / light theme — system-aware with localStorage override
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI | React 19, Tailwind CSS 4 |
| Language | TypeScript 5 |
| Database | Neon (PostgreSQL serverless) — sessions, lead lists |
| Object storage | Cloudflare R2 — large lead-list CSVs (S3-compatible, presigned URLs) |
| CSV parsing | PapaParse |
| Icons | Lucide React |
| Class utility | clsx + tailwind-merge |
| Deployment | Vercel |
| Dev tooling | Turbopack |
src/
middleware.ts ← auth gate: verifies hf_session cookie, redirects to /login or 401s API requests
app/
page.tsx ← entry point, OG metadata from URL params
login/ ← email/password sign-in page
setup/ ← first-run admin account creation
admin/users/ ← admin-only user management UI (create/delete users, toggle permissions)
f/[slug]/page.tsx ← read-only shared list view (SharedView, password-gated, public — no login)
api/
auth/ ← login, logout, setup, current-user (me) routes
users/ ← admin-only user CRUD
sessions/ ← CRUD for saved filter sessions (Neon DB)
lead-lists/ ← CRUD for lead list management (Neon DB)
lead-lists/master/ ← paginated union of all normalized lead lists, for the home view
lead-lists/master/rows/ ← paginated + filtered master rows (25/page)
lead-lists/master/export/ ← export all matching master rows (CSV/JSON/labelled)
lead-lists/[id]/sample/ ← lightweight sample + column types for opening a lead list
lead-lists/[id]/rows/ ← paginated + filtered rows for a lead list (25/page)
lead-lists/[id]/export/ ← export all matching rows for a lead list
lead-lists/[id]/import/ ← chunked import of additional rows into a lead list
lead-lists/upload-url/ ← presigned R2 upload URL for large CSVs
components/
AuthProvider.tsx ← client context exposing the current user + hasPermission() checks
UserMenu.tsx ← Sidebar user menu (email, admin link, log out)
FilterPage.tsx ← root component, orchestrates all panels
DataInput/ ← CSV upload + column mapping
FilterBuilder/ ← AND/OR rule group editor
ResultsTable/ ← paginated results + stats bar
ActionBar/ ← export, undo/redo, share button (opens ShareModal)
Sessions/ ← save/load/password-protect Saved Views; ShareModal for one-click client links
SharedView/ ← read-only capped table + CSV download for /f/[slug]
LeadLists/ ← lead list creation and management
LookupWizard/ ← guided enrichment workflow
lib/
auth.ts ← session cookie helpers, getCurrentUser(), hasPermission()
crypto.ts ← Edge-safe HMAC session token signing/verification (Web Crypto)
passwords.ts ← Node-only password hashing (scrypt)
usersDb.ts ← user CRUD + permissions (canManageData/canManageViews/canExport)
datasetView.ts ← shared pagination/filtering helper used by all rows/sample/export routes
filterEngine.ts ← core AND/OR evaluation logic
urlState.ts ← URL serialization/deserialization (address-bar state sync)
sharedView.ts ← computes the capped, filtered view served at /f/[slug]
saveLeadList.ts ← shared chunked-upload helper for auto-saving datasets as lead lists
db.ts ← Neon DB client + schema (incl. contacts/companies/lead_list_rows, users)
sessionsDb.ts ← session queries
leadListsDb.ts ← lead list queries
columnMapping.ts ← suggests contact/company field mapping from CSV headers
masterDb.ts ← chunked import + dedup of lead list rows into contacts/companies
r2.ts ← Cloudflare R2 client + presigned URL helpers
lookupEngine.ts ← lookup/enrichment logic
types.ts ← shared TypeScript types
- Node.js 18+
- A Neon PostgreSQL database (for sessions and lead lists)
- A Cloudflare R2 bucket (for large lead-list CSVs)
npm installCreate a .env.local file:
DATABASE_URL=your_neon_connection_string
R2_ACCOUNT_ID=your_cloudflare_account_id
R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
R2_BUCKET_NAME=your_r2_bucket_name
# Signs the hf_session auth cookie (HMAC-SHA256). Generate with:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
SESSION_SECRET=your_random_32_byte_hex_secretThe R2 bucket needs a CORS policy allowing GET/PUT from your app's origin(s) so the browser can upload/download CSVs directly:
[
{
"AllowedOrigins": ["http://localhost:3007", "https://your-deployment.vercel.app"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]Run database migrations (SQL in /data/ directory) to create the sessions and lead_lists tables.
npm run devOpens at http://localhost:3007. On first run, you'll be redirected to /setup to create the first account — it's automatically granted admin access. Admins can create additional users and set their permissions at /admin/users.
npm run build
npm startDeployed to Vercel. Set DATABASE_URL and the four R2_* variables in Vercel environment variables (Settings → Environment Variables).
The filter core works fully client-side. Sessions and lead lists require the database connection. Lead lists over ~3MB require R2 to be configured.