← work / b2b-portal

liveSole engineer for a client: requirements, build, data migration and post-launch operations

B2B Lead Operations Platform

Client portal, internal portal and reporting backend for a lead-generation business

// the problem

A business running outreach across several platforms had its results scattered: each platform its own dashboard, reporting assembled by hand, clients emailing to ask how their campaigns were doing. This puts campaign results, pipeline and billing behind one login for the team and one for each client.

// context

Built for a lead-generation business and its clients, so the team and each client read results from the same data. It replaced an earlier FastAPI service on Render whose report scheduler was an outside GitHub Actions job calling the API every 30 minutes.

// what I built

  1. Thirteen systems on a Cloudflare Worker and D1 backend: 58 endpoints across 20 route modules, and a schema of nearly 30 tables grown through 31 migrations as the real process revealed itself.
  2. Six outside systems behind one reporting surface: Instantly, HeyReach and Smartlead for campaigns, Slack for alerts, Google Sheets and Docs for exports, and HubSpot over OAuth. Each has its own auth and rate limits.
  3. Client access without passwords: single-use magic links that expire and are burned on use, emailed to the contact, with two-way messaging, unread badges and read receipts, file sharing and comments behind it.
  4. Stored integration keys masked on read, so the portal never echoes a customer credential back to the screen, and an audit log over privileged actions.
  5. Three scheduled jobs: report generation hourly on weekdays, revenue and expense rollups on the first of the month, and a pipeline sync from Sheets every four hours, offset so it never overlaps the reports.
  6. Three React frontends: the client-facing portal, an internal admin portal, and the marketing site.

// key decisions

  1. Move scheduling and storage onto one Worker

    The first version spread the API, database and scheduler across three hosts, and much of its history went into tuning that scheduler. The rebuild put API, database, files and cron triggers on one Worker with D1 and R2, so timezone handling and duplicate prevention (a 26-hour window per customer) live in one place.

  2. D1 over Postgres, with the migration cost paid up front

    D1 removed a separately hosted database, but SQLite cannot drop a constraint: one schema change left foreign keys in 18 tables pointing at a renamed table, and two migrations rebuilt them. Data came across with a re-runnable script that converts types and inserts in batches of 50 to stay under D1 limits.

  3. Magic links for clients, sessions for staff

    Clients get a link per company that expires in 30 minutes, is burned on first use and opens a 30-day session, so clients never manage a password. Staff keep password login with a 7-day HttpOnly cookie and user, admin and super_admin roles.

  4. One snapshot model across providers

    Instantly and Smartlead return the same metrics type, and snapshots from every provider go into one table with a platform column. Reports read one shape regardless of source.

// results

  1. 31 migrations defining 29 tables.
  2. About 14,200 lines of Worker TypeScript in 20 route modules, with 3 cron triggers.

// screens

The internal portal documents itself: all thirteen systems behind the product, each with the tables and endpoints it owns. Email on Resend, cron-driven weekly reports, per-customer API keys with live connection testing.
The rest of them: token-based client login, two-way support messaging with read receipts, role management, per-customer automation profiles, 52-week campaign trends and the KPI tracker.
What the customer sees: their own pipeline by month and status, on the same data the team works from rather than a report someone assembles by hand.