A comprehensive, white-label SaaS platform designed specifically for lead generation agencies. Manage multiple clients, campaigns, and leads with seamless integrations to Instantly.ai, Smartlead, and HubSpot. Features multi-tenant architecture, real-time analytics, infrastructure monitoring, and automated billing through Stripe. Built with Next.js 16, Supabase, and TypeScript for enterprise-grade performance and scalability.
- Overview
- Features
- Tech Stack
- Architecture
- Getting Started
- Environment Variables
- Database Schema
- API Reference
- Provider Integrations
- Webhooks
- Cron Jobs
- Infrastructure Health Monitoring
- Email Notifications
- Billing & Subscriptions
- Multi-Tenancy & Impersonation
- SEO
- Deployment
- Project Structure
- Contributing
Agency Lead Operations SaaS is a multi-tenant agency management platform designed for cold email outreach agencies. Each agency owner signs up, selects a plan, and gets their own fully isolated workspace. A platform admin can oversee all agencies via impersonation.
- Admin Portal: Full control over clients, campaigns, leads, analytics, infrastructure, and settings
- Client Portal: Role-restricted dashboard for clients to view campaigns, manage lead workflows, and track results
- Multi-Provider Integration: Bidirectional sync with Instantly.ai and Smartlead for campaigns and leads, plus HubSpot CRM sync
- Infrastructure Monitoring: Email account health tracking, DNS validation (SPF/DKIM/DMARC), and warmup monitoring
- Real-time Updates: Webhook-driven data sync from email providers with auto-refreshing dashboards
- Email Notifications: Automated alerts for positive replies and weekly stats reports via Resend
- Multi-Tenancy: Full data isolation between agency owners via
owner_idscoping on all tables - Stripe Billing: 3-tier subscription plans with 14-day free trial, billing portal, and webhook-driven sync
- Platform Admin: Impersonation system to view any agency's workspace as they see it
- Real-time analytics filtered by time period (this week, this month, this quarter, all time)
- Leads contacted, emails sent, replies received, positive replies (opportunities)
- Reply rate calculation
- Clickable stats that drill down into the detailed leads view
- Auto-refresh every 30 seconds
- All data scoped to the current agency owner
- Create and delete clients (enforced by subscription plan limits)
- View details, settings per client
- Search and filter customers (active, archived, demo)
- Customer cards showing campaign count and key metrics
- Link campaigns from Instantly or Smartlead to clients
- View campaign performance: total leads, leads contacted (progress bar), emails sent, replies, positive replies
- Sync campaigns and leads from providers
- View campaign email copy/sequences
- Click campaigns to view detailed analytics with variant breakdown
- Export campaign leads to CSV
- Delete campaigns (with confirmation)
- Detailed campaign statistics with variant-level analytics
- List of positive replies with contact info
- Email thread viewer per lead
- Campaign sequence viewer (multi-step, A/B/C variants)
- Campaign progress visualization
- Campaign diagnostics
- View all leads across all clients (132,000+ supported)
- Server-side filtering by client, status, and positive reply flag
- Lead statuses:
contacted,opened,clicked,replied,booked,won,lost,not_interested - Pagination: 100 leads per page with navigation
- CSV export options: current filter, positive replies, all replies, no response, all leads
- Click any lead to view/edit details in a slide-out panel
- View complete lead information (email, phone, company, LinkedIn, domain)
- Update lead status through the workflow
- Add/edit notes
- View email thread history (outbound and inbound messages)
- Track email engagement: open count, click count, reply count
- Track positive replies through the sales funnel
- Workflow states: Responded, Meeting Scheduled, Closed Won, Closed Lost
- Clickable status filter toggles: Click any status badge (Responded, Meetings, Won, Lost) to filter the workflow view; click again to clear
- Collapsible UI with summary stats in header
- Quick one-click status updates with date/time selection for meetings
- Notes per workflow stage
- Reply date tracking:
responded_atpopulated from Smartleadreply_timeduring sync
- Centralized lead database across all clients and campaigns
- Internal note-taking system for agency operations
- Track API credits and subscription status across providers
- Live sync of remaining credits
- Interactive 5-step campaign builder mockup (coming soon):
- Knowledge Base — Upload/toggle MD files with best practices
- Reference Campaigns — Select top-performing campaigns with positive reply stats
- Campaign Builder — Chat-style Q&A with Fireflies call recording integration
- Copy Generation — Iterative V1→V2→V3 with feedback loop and follow-up sequences
- A/B Testing Matrix — 5 categories (Offer, Subject, CTA, First Line, Follow-up) × 3 variants
- Coming soon placeholder cards for:
- Google Maps — Local business scraping by category and location
- LinkedIn Sales Navigator — B2B decision-maker extraction
- Blitz API — Bulk lead enrichment from domain/CSV uploads
- Monitor email account health across Instantly and Smartlead
- DNS validation: SPF, DKIM, and DMARC record checking
- Warmup tracking: reputation scores, emails sent/received
- Daily health snapshots for trend analysis
- Client-to-account assignment
- Filter by provider, status, or client
- Auto-refresh every 30 seconds
- Agency Owners (platform admin only): List all agency owners with client counts, "View Dashboard" button for impersonation
- Agency logo upload
- Agency branding (name, primary color, sender name/email)
- API key management (Instantly, Smartlead)
- Email service configuration (Resend)
- Current subscription plan display
- Trial days remaining indicator
- "Manage Billing" button → Stripe customer portal
- Trial and past-due banners in admin layout
- OAuth login via Google or Microsoft
- Context-aware login page (recognizes invitation tokens)
- View all campaigns assigned to their account
- Lead statistics: contacted, replied, positive replies, meetings, deals
- Lead workflow management (respond, schedule meetings, close deals)
- Add notes to leads
- Cannot link/delete campaigns or access other admin routes
- OAuth: Google and Microsoft sign-in via Supabase Auth
- Role-Based Access: Platform Admin → Admin → Client (Owner/Manager/Member/Viewer)
- Client Invitations: Admin invites client users by email; auto-linked on first login
- Middleware Protection: All
/admin/*routes protected; client users restricted to their linked client pages - Context-Aware Login: Agency owners see social proof + free trial CTA; invited clients see a tailored "Your dashboard is ready" view
- Landing Page (
/): Hero with CSS dashboard mockup, stats bar, problem/solution cards, "See it in Action" showcase, 7-feature grid, 6 testimonials with professional headshot photos, and CTA - Features Page (
/features): Detailed breakdown of client portals, analytics, and lead management with mock UI visuals - Pricing Page (
/pricing): 3-tier plan comparison (Starter $49, Growth $99, Agency $249), feature comparison table, FAQ section - Plan Selection (
/choose-plan): Post-signup plan selection with 14-day free trial on all plans
- Root metadata (
layout.tsx): OpenGraph tags, Twitter cards, keyword targeting, robots directives - Page-specific metadata: Unique title/description for homepage, features, and pricing pages
- Sitemap (
/sitemap.xml): Auto-generated fromsitemap.tswith all public routes - Robots (
/robots.txt): Auto-generated fromrobots.ts— allows marketing pages, blocks/admin,/dashboard,/api,/auth - JSON-LD structured data:
- Homepage:
Organization,SoftwareApplication(with pricing and rating),WebSiteschemas - Pricing:
FAQPageschema for Google rich snippet FAQ accordion
- Homepage:
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 16.x | App Router, Server Components, API Routes |
| React | 19.x | UI framework |
| TypeScript | 5.x | Type-safe development |
| Supabase | Latest | PostgreSQL database, Auth (OAuth), Row Level Security |
| Stripe | Latest | Subscription billing, checkout, customer portal |
| Tailwind CSS | 4.x | Utility-first styling |
| shadcn/ui | Latest | UI component library (Radix UI primitives) |
| Lucide React | Latest | Icon library |
| Resend | Latest | Transactional email delivery |
| React Email | Latest | Email template rendering |
| DOMPurify | Latest | HTML sanitization for email content |
| Docker | Latest | Containerized deployment |
| Caddy | 2.x | Reverse proxy with automatic HTTPS |
┌─────────────────────────────────────────────────────────────────────┐
│ Client Browser │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ Admin Portal │ │ Client Portal │ │ Marketing Pages │ │
│ │ /admin/* │ │ /admin/clients│ │ /, /features, /pricing │ │
│ └──────────────┘ └───────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Next.js Application │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ Server │ │ API Routes │ │ Webhook Handlers │ │
│ │ Components │ │ /api/* │ │ /api/webhooks/* │ │
│ └──────────────┘ └───────────────┘ └──────────────────────────┘ │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ Middleware │ │ Auth │ │ Cron Endpoints │ │
│ │ (RLS check) │ │ (OAuth) │ │ /api/cron/* │ │
│ └──────────────┘ └───────────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌────────────────┐ ┌──────────────────────────┐
│ Supabase │ │ Email / Pay │ │ External Providers │
│ ┌────────────┐ │ │ ┌──────────┐ │ │ ┌────────────────────┐ │
│ │ PostgreSQL │ │ │ │ Resend │ │ │ │ Instantly API │ │
│ │ + RLS │ │ │ ├──────────┤ │ │ │ Smartlead API │ │
│ ├────────────┤ │ │ │ Stripe │ │ │ │ HubSpot API │ │
│ │ Auth │ │ │ └──────────┘ │ │ └────────────────────┘ │
│ │ (OAuth) │ │ └────────────────┘ │ ┌────────────────────┐ │
│ ├────────────┤ │ │ │ Google DoH (DNS) │ │
│ │ Storage │ │ │ └────────────────────┘ │
│ └────────────┘ │ └──────────────────────────┘
└──────────────────┘
User clicks "Sign in with Google/Microsoft"
↓
Supabase OAuth redirect
↓
/auth/callback
↓
Check profiles table for role + pending invitations
↓
├── New user with invitation → Link to client, redirect to client dashboard
├── New admin (no subscription) → Redirect to /choose-plan
├── role = 'admin' → Redirect to /admin (Command Center)
└── role = 'client' → Redirect to /admin/clients/[clientId] (restricted view)
Admin creates invitation (email)
↓
Invitation stored in client_invitations table
↓
Invited user clicks login link (/login?inviteToken=...)
↓
Context-aware login page shows client-specific messaging
↓
/auth/callback checks for pending invitation matching email
↓
Auto-links user to client via client_users table
↓
Invitation marked as accepted
API Request → requireAdmin() → getEffectiveOwnerId()
↓
├── Platform admin + impersonation cookie → returns impersonated owner UUID
├── Platform admin (no impersonation) → returns admin's own UUID
└── Regular admin → returns their own UUID
↓
All queries filter by .eq("owner_id", effectiveOwnerId)
↓
Complete data isolation between agency owners
| Feature | Platform Admin | Admin (Agency Owner) | Client (Owner/Manager) | Client (Member/Viewer) |
|---|---|---|---|---|
| View own dashboard | Yes | Yes | N/A | N/A |
| Impersonate other agencies | Yes | No | No | No |
| Manage clients | Own + impersonated | Own only | No | No |
| View client dashboard | All (via impersonation) | Own clients | Linked clients only | Linked clients only |
| Manage lead workflow | Yes | Yes | Yes | View only |
| Link/delete campaigns | Yes | Yes | No | No |
| Infrastructure monitoring | Yes | Yes | No | No |
| Manage settings | Yes | Yes | No | No |
| Agency Owners list | Yes | No | No | No |
| Billing management | Exempt (free) | Yes | No | No |
Before you begin, ensure you have the following installed:
- Node.js 18+ (Download)
- npm or yarn package manager
- Git for version control
You'll also need accounts with the following services:
- Supabase account (supabase.com)
- Stripe account for billing (stripe.com)
- Resend account for email notifications (resend.com)
# Clone the repository
git clone https://github.com/upstackpilot0710/agency-lead-ops-saas.git
cd agency-lead-ops-saas
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env.local
# Edit .env.local with your credentials (see Environment Variables section)
# Run database migrations in Supabase SQL Editor (in order):
# 1. supabase-schema.sql — Core tables: clients, campaigns, leads
# 2. supabase-schema-v2.sql — Denormalized fields, multi-provider support
# 3. supabase-settings.sql — Settings table
# 4. supabase-preserve-leads.sql — Triggers to preserve lead data on deletion
# 5. supabase-analytics.sql — Analytics views, snapshots, and functions
# 6. supabase-add-icp.sql — ICP (Ideal Customer Profile) fields
#
# Then run the numbered migrations in supabase/migrations/:
# 7. 20260303_add_reply_tokens.sql — Reply token table for smart email redirects
# 8. 20260304_add_stripe_billing.sql — Stripe subscriptions table + profile fields
# 9. 20260304_add_multi_tenancy.sql — owner_id on clients/settings, is_platform_admin
# 10. 20260305_add_owner_scoping.sql — owner_id on subscriptions, lead_sources, email_accounts
# Start development server
npm run devnpm run dev # Start development server (http://localhost:3000)
npm run build # Build for production
npm start # Start production server
npm run lint # Run ESLintCreate a .env.local file in the project root:
# Supabase (Required)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# App URL (Required)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Stripe Billing (Required for subscriptions)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_GROWTH=price_...
STRIPE_PRICE_AGENCY=price_...
# Email Notifications (Required for notifications)
RESEND_API_KEY=re_your-resend-api-key
# Instantly API (Optional — can be configured via Admin Settings UI)
INSTANTLY_API_KEY=your-instantly-api-key
INSTANTLY_WEBHOOK_SECRET=your-webhook-secret
# Smartlead API (Optional — can be configured via Admin Settings UI)
SMARTLEAD_API_KEY=your-smartlead-api-key
# Cron Jobs (Required for automated stats sync and reports)
CRON_SECRET=your-random-secret-string| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Yes | Your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Supabase anonymous/public key (client-side) |
SUPABASE_SERVICE_ROLE_KEY |
Yes | Supabase service role key (server-side only, bypasses RLS) |
NEXT_PUBLIC_APP_URL |
Yes | Your app's public URL (used for links in emails, webhooks) |
STRIPE_SECRET_KEY |
Yes | Stripe secret key for server-side API calls |
STRIPE_WEBHOOK_SECRET |
Yes | Secret for validating Stripe webhook payloads |
STRIPE_PRICE_STARTER |
Yes | Stripe Price ID for Starter plan ($49/mo) |
STRIPE_PRICE_GROWTH |
Yes | Stripe Price ID for Growth plan ($99/mo) |
STRIPE_PRICE_AGENCY |
Yes | Stripe Price ID for Agency plan ($249/mo) |
RESEND_API_KEY |
No | Resend API key for sending email notifications |
INSTANTLY_API_KEY |
No | Instantly API key (Settings → Integrations → API) |
INSTANTLY_WEBHOOK_SECRET |
No | Secret for validating Instantly webhook payloads |
SMARTLEAD_API_KEY |
No | Smartlead API key for campaign and account sync |
CRON_SECRET |
No | Shared secret for authenticating cron job endpoints (?secret=...) |
User profiles linked to Supabase Auth.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key (references auth.users) |
email |
text | User email |
role |
text | 'admin' or 'client' |
full_name |
text | Display name |
is_platform_admin |
boolean | Whether user is the platform super-admin (default false) |
stripe_customer_id |
text | Stripe customer ID (unique) |
Agency clients (companies being served). Scoped by owner_id.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
owner_id |
uuid | FK → auth.users — the agency owner who created this client |
name |
text | Client company name |
logo_url |
text | Client logo URL |
website |
text | Client website |
notes |
text | Internal notes |
product_service |
text | What the client sells |
acv |
numeric | Average Contract Value |
tcv |
numeric | Total Contract Value |
verticals |
text[] | Target industries |
tam |
integer | Total Addressable Market (lead count) |
target_daily_emails |
integer | Daily sending target |
is_active |
boolean | Active flag |
created_at |
timestamptz | Creation timestamp |
Links users to clients with role-based access.
| Column | Type | Description |
|---|---|---|
client_id |
uuid | FK → clients(id) |
user_id |
uuid | FK → auth.users(id) |
role |
text | 'owner', 'manager', 'member', or 'viewer' |
Primary key: (client_id, user_id)
Pending invitations for client users.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
client_id |
uuid | FK → clients(id) |
email |
text | Invited user's email |
invited_by |
uuid | Admin who sent the invite |
created_at |
timestamptz | When the invite was created |
accepted_at |
timestamptz | When accepted (NULL if pending) |
Email campaigns linked to external providers.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
client_id |
uuid | FK → clients(id) |
instantly_campaign_id |
text | Legacy Instantly campaign ID |
provider_type |
text | 'instantly', 'smartlead', etc. |
provider_campaign_id |
text | Provider-specific campaign ID |
name |
text | Campaign display name |
original_name |
text | Original name from provider |
copy_body |
text | Email template/copy |
is_active |
boolean | Active flag |
last_synced_at |
timestamptz | Last sync timestamp |
cached_emails_sent |
integer | Cached total emails sent |
cached_reply_count |
integer | Cached total replies |
cached_emails_bounced |
integer | Cached bounced count |
cached_leads_count |
integer | Cached total leads |
sync_in_progress |
boolean | Whether a sync is currently running |
sync_started_at |
timestamptz | When the current sync started (5-min timeout) |
Email sequence steps with A/B/C variants.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
campaign_id |
uuid | FK → campaigns(id) |
sequence_index |
integer | Position in sequence |
step_number |
integer | Step number |
variant |
text | 'A', 'B', or 'C' |
subject |
text | Email subject line |
body_text |
text | Plain text body |
body_html |
text | HTML body |
delay_days |
integer | Days to wait before sending |
delay_hours |
integer | Hours to wait before sending |
All leads with denormalized data for preservation.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
campaign_id |
uuid | FK → campaigns(id) |
client_id |
uuid | Denormalized client ID |
client_name |
text | Denormalized client name |
campaign_name |
text | Denormalized campaign name |
email |
text | Lead email address |
first_name |
text | Lead first name |
last_name |
text | Lead last name |
company_name |
text | Lead company |
company_domain |
text | Lead company domain |
phone |
text | Lead phone number |
linkedin_url |
text | Lead LinkedIn profile |
status |
text | See lead statuses below |
is_positive_reply |
boolean | Whether reply was positive |
has_replied |
boolean | Whether lead has replied |
responded_at |
timestamptz | When the lead replied (auto-populated from provider reply_time during sync, or set manually) |
meeting_at |
timestamptz | Scheduled meeting date/time |
closed_at |
timestamptz | When deal was closed |
deal_value |
numeric | Value of the deal |
notes |
text | Admin/workflow notes |
instantly_lead_id |
text | Instantly lead ID |
email_open_count |
integer | Number of email opens |
email_click_count |
integer | Number of link clicks |
email_reply_count |
integer | Number of replies |
metadata |
jsonb | Additional provider-specific data |
created_at |
timestamptz | Creation timestamp |
updated_at |
timestamptz | Last update timestamp |
Lead Status Values:
| Status | Description |
|---|---|
contacted |
Initial outreach sent |
opened |
Email was opened |
clicked |
Link in email was clicked |
replied |
Lead replied to email |
booked |
Meeting was booked |
won |
Deal closed (won) |
lost |
Deal closed (lost) |
not_interested |
Lead declined |
Email thread for each lead (outbound and inbound messages).
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
lead_id |
uuid | FK → leads(id) |
campaign_id |
uuid | FK → campaigns(id) |
provider_email_id |
text | Provider-specific email ID |
provider_thread_id |
text | Provider-specific thread ID |
direction |
text | 'outbound' or 'inbound' |
from_email |
text | Sender email |
to_email |
text | Recipient email |
subject |
text | Email subject |
body_text |
text | Plain text body |
body_html |
text | HTML body |
sequence_step |
integer | Which sequence step this is |
sent_at |
timestamptz | When the email was sent |
opened_at |
timestamptz | When the email was opened |
replied_at |
timestamptz | When a reply was received |
created_at |
timestamptz | Record creation timestamp |
Activity log for leads.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
lead_id |
uuid | FK → leads(id) |
user_id |
uuid | User who created the activity |
type |
text | 'call', 'meeting', 'email', 'note', 'status_change' |
title |
text | Activity title |
description |
text | Activity description |
scheduled_at |
timestamptz | Scheduled time (if applicable) |
completed_at |
timestamptz | Completion time |
Tracking events from email providers.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
lead_id |
uuid | FK → leads(id) |
campaign_id |
uuid | FK → campaigns(id) |
event_type |
text | 'sent', 'opened', 'clicked', 'replied', 'bounced' |
instantly_event_id |
text | Provider event ID |
timestamp |
timestamptz | When the event occurred |
Multi-provider API key storage (per-client or global).
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
client_id |
uuid | FK → clients(id) (NULL for global) |
provider_type |
text | 'instantly', 'smartlead', 'lemlist', 'apollo' |
api_key |
text | Encrypted API key |
workspace_id |
text | Provider workspace/account ID |
is_active |
boolean | Active flag |
label |
text | Display label |
Subscription records synced from Stripe.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
user_id |
uuid | FK → auth.users(id) |
stripe_subscription_id |
text | Stripe subscription ID (unique) |
stripe_customer_id |
text | Stripe customer ID |
plan |
text | 'starter', 'growth', or 'agency' |
status |
text | 'active', 'trialing', 'past_due', 'canceled', 'incomplete' |
current_period_start |
timestamptz | Current billing period start |
current_period_end |
timestamptz | Current billing period end |
trial_start |
timestamptz | Trial start date |
trial_end |
timestamptz | Trial end date |
cancel_at_period_end |
boolean | Whether subscription cancels at period end |
created_at |
timestamptz | Record creation timestamp |
updated_at |
timestamptz | Last update timestamp |
Short-lived tokens for smart email reply redirects.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key (used as the token) |
lead_email |
text | The lead's email address |
subject |
text | Email subject for reply |
body |
text | Quoted thread body |
lead_id |
uuid | FK → leads(id) |
created_at |
timestamptz | Creation timestamp (72-hour TTL) |
used_at |
timestamptz | First access timestamp |
Central registry of email accounts from Instantly and Smartlead.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
owner_id |
uuid | FK → auth.users — the agency owner |
provider_type |
text | 'instantly' or 'smartlead' |
provider_account_id |
text | Provider-specific account ID |
email |
text | Email address |
client_id |
uuid | FK → clients(id) (manual assignment) |
domain |
text | Generated from email address |
status |
text | 'active', 'error', 'disconnected', 'paused' |
warmup_enabled |
boolean | Whether warmup is active |
warmup_reputation |
integer | Reputation score (0–100) |
warmup_emails_sent |
integer | Warmup emails sent |
warmup_emails_received |
integer | Warmup emails received |
daily_limit |
integer | Daily sending limit |
last_synced_at |
timestamptz | Last sync timestamp |
Unique constraint: (provider_type, email)
Daily snapshots for trend analysis.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
email_account_id |
uuid | FK → email_accounts(id) |
snapshot_date |
date | Snapshot date |
status |
text | Account status at snapshot time |
warmup_reputation |
integer | Reputation at snapshot time |
warmup_emails_sent |
integer | Warmup sent count |
warmup_emails_received |
integer | Warmup received count |
emails_sent_today |
integer | Emails sent that day |
Unique constraint: (email_account_id, snapshot_date)
DNS validation cache for SPF/DKIM/DMARC.
| Column | Type | Description |
|---|---|---|
id |
uuid | Primary key |
domain |
text | Domain name (unique) |
has_spf |
boolean | SPF record exists |
spf_record |
text | Raw SPF record |
spf_valid |
boolean | SPF record is valid |
has_dkim |
boolean | DKIM record found |
dkim_selector |
text | DKIM selector used |
dkim_record |
text | Raw DKIM record |
dkim_valid |
boolean | DKIM record is valid |
has_dmarc |
boolean | DMARC record exists |
dmarc_record |
text | Raw DMARC record |
dmarc_policy |
text | 'none', 'quarantine', or 'reject' |
dmarc_valid |
boolean | DMARC record is valid |
health_score |
integer | Generated score (0–100) based on DNS records |
last_checked_at |
timestamptz | Last check timestamp |
Per-owner application settings (keyed by owner_id + key).
| Column | Type | Description |
|---|---|---|
id |
integer | Auto-increment primary key |
owner_id |
uuid | FK → auth.users — the agency owner |
key |
text | Setting key (e.g., instantly_api_key, agency_logo_url) |
value |
text | Setting value |
updated_at |
timestamptz | Last update timestamp |
Unique constraint: (key, owner_id)
All tables have RLS enabled with owner-scoped policies:
- Platform admin users can access all data across all owners
- Admin users can only access data where
owner_idmatches their user ID - Client users can only access data for clients linked via
client_users - Service role key bypasses RLS for webhook handlers and cron jobs
Run these in order in the Supabase SQL Editor:
| File | Purpose |
|---|---|
supabase-schema.sql |
Core tables: clients, campaigns, leads, profiles |
supabase-schema-v2.sql |
Denormalized fields, multi-provider support, lead_emails, client_users, client_invitations |
supabase-settings.sql |
Settings table |
supabase-preserve-leads.sql |
Triggers to preserve lead data when clients/campaigns are deleted |
supabase-analytics.sql |
Analytics views, snapshot tables, and aggregation functions |
supabase-add-icp.sql |
ICP fields on clients (product_service, acv, tcv, verticals, tam) |
supabase/migrations/20260303_add_reply_tokens.sql |
Reply tokens table for smart email redirects |
supabase/migrations/20260304_add_stripe_billing.sql |
Stripe subscriptions table, stripe_customer_id on profiles |
supabase/migrations/20260304_add_multi_tenancy.sql |
owner_id on clients/settings, is_platform_admin on profiles |
supabase/migrations/20260305_add_owner_scoping.sql |
owner_id on subscriptions, lead_sources, email_accounts |
GET /api/admin/settings # Get all settings (sensitive values masked)
POST /api/admin/settings # Update a setting
DELETE /api/admin/settings?key=x # Clear a setting
POST /api/admin/settings/logo # Upload agency logo
GET /api/admin/analytics?period=this_week
POST /api/admin/analytics/sync # Trigger analytics sync from providers
Query Parameters:
period:all_time|this_week|this_month|this_quarter
Response:
{
"period": "all_time",
"leads_contacted": 132589,
"emails_sent": 132189,
"replies": 4218,
"opportunities": 322,
"reply_rate": 3.19,
"data_source": "campaigns_cached"
}GET /api/admin/leads/export?export=positive
Export Types:
current— Current filter results (supportsclient,status,positiveparams)positive— All positive repliesreplied— All repliesno_response— Contacted but no replyall— All leads
GET /api/admin/customers # List all clients (scoped by owner)
POST /api/admin/customers # Create client (enforces plan limit)
GET /api/admin/customers/[id] # Get client details
PUT /api/admin/customers/[id] # Update client
DELETE /api/admin/customers/[id] # Delete client
GET /api/admin/agencies # List all agency owners with client counts
POST /api/admin/impersonate # Start impersonating an agency owner
DELETE /api/admin/impersonate # Stop impersonating
GET /api/admin/invitations # List pending invitations
POST /api/admin/invitations # Create invitation
DELETE /api/admin/invitations/[id] # Revoke invitation
GET /api/admin/lead-database # Query centralized lead database
GET /api/admin/notes # List notes
POST /api/admin/notes # Create note
GET /api/admin/subscriptions # Get subscription/credit status
GET /api/admin/notification-preferences
POST /api/admin/notification-preferences
POST /api/stripe/checkout # Create Stripe checkout session (14-day trial)
POST /api/stripe/portal # Create Stripe billing portal session
GET /api/stripe/subscription # Get current subscription status + client limit
POST /api/reply-tokens # Create a reply token (stores compose data)
GET /api/reply-tokens/[token] # Fetch token data (72-hour TTL)
GET /api/clients/[clientId] # Get client details
PATCH /api/clients/[clientId] # Update client
GET /api/clients/[clientId]/campaigns # List client campaigns
GET /api/clients/[clientId]/leads # Get leads (?positive=true)
POST /api/clients/[clientId]/logo # Upload client logo
GET /api/clients/[clientId]/invitations # List invitations
POST /api/clients/[clientId]/invitations # Create invitation
POST /api/clients/[clientId]/sync-positive # Sync positive replies
GET /api/clients/[clientId]/stats-settings # Get stats display settings
POST /api/clients/[clientId]/stats-settings # Update stats display settings
GET /api/clients/[clientId]/notification-preferences # Get notification prefs
POST /api/clients/[clientId]/notification-preferences # Update notification prefs
GET /api/clients/[clientId]/tool-links # Get external tool links
POST /api/clients/[clientId]/hubspot # Sync leads to HubSpot
POST /api/clients/[clientId]/test-hubspot # Test HubSpot connection
PATCH /api/leads/[leadId]/workflow # Update lead workflow status
Actions:
| Action | Description |
|---|---|
mark_responded |
Mark lead as responded |
schedule_meeting |
Set meeting date/time |
close_won |
Mark deal as won |
close_lost |
Mark deal as lost |
update_notes |
Update workflow notes |
revert_status |
Revert to previous status |
GET /api/campaigns/[id]/details # Campaign details with stats
GET /api/campaigns/[id]/leads # Campaign leads
GET /api/campaigns/[id]/variant-analytics # A/B/C variant performance
GET /api/campaigns/[id]/export-leads # Export campaign leads
GET /api/campaigns/[id]/diagnose # Campaign diagnostics
POST /api/campaigns/[id]/sync-leads # Sync leads from provider
POST /api/campaigns/[id]/sync-emails # Sync email threads
POST /api/campaigns/[id]/recalculate # Recalculate campaign stats
POST /api/campaigns/[id]/generate-skill # Generate campaign skill report
DELETE /api/campaigns/[id] # Delete campaign
GET /api/providers/[provider]/campaigns # List campaigns from provider
POST /api/providers/[provider]/validate # Validate provider API key
Supported providers: instantly, smartlead
GET /api/instantly/status # Check API connection
GET /api/instantly/campaigns # List Instantly campaigns
POST /api/instantly/campaigns # Sync campaigns to DB
GET /api/instantly/campaigns/[id] # Get campaign details
POST /api/instantly/campaigns/[id] # Activate/pause campaign
GET /api/instantly/campaigns/analytics # Get campaign analytics
GET /api/instantly/leads # List leads from Instantly
POST /api/instantly/leads # Sync leads to DB
GET /api/instantly/accounts # List email accounts
POST /api/instantly/sync # Full sync (campaigns + leads)
POST /api/instantly/validate-key # Validate API key
POST /api/instantly/refresh-status # Refresh campaign statuses
GET /api/smartlead/campaigns # List Smartlead campaigns
GET /api/smartlead/status # Check Smartlead API connection
POST /api/smartlead/validate-key # Validate API key
GET /api/admin/infrastructure/stats # Dashboard statistics
GET /api/admin/infrastructure/accounts # List accounts (with filters)
PATCH /api/admin/infrastructure/accounts/[id] # Assign client to account
POST /api/admin/infrastructure/sync # Sync from Instantly/Smartlead
GET /api/admin/infrastructure/dns # Get cached domain health
POST /api/admin/infrastructure/dns # Check specific domains
PATCH /api/admin/infrastructure/dns # Refresh all domain checks
GET /api/admin/infrastructure/history # Historical health snapshots
POST /api/admin/infrastructure/history # Create daily snapshot
Setup:
- Get your API key from Instantly Settings → Integrations → API
- Add to
INSTANTLY_API_KEYin.env.localor configure via Admin Settings UI - Configure webhook in Instantly to point to
/api/webhooks/instantly/[campaignId]
API Client (src/lib/instantly/):
| File | Purpose |
|---|---|
client.ts |
Base HTTP client with Bearer token auth |
campaigns.ts |
List, activate, pause campaigns |
leads.ts |
List, create, update leads |
emails.ts |
Fetch email threads |
analytics.ts |
Campaign and account analytics |
accounts.ts |
Email account management |
types.ts |
TypeScript type definitions |
Sync Process:
- Fetch campaigns from Instantly API
- Create/update local campaign records in Supabase
- Sync leads for each campaign
- Sync email threads for leads
- Sync analytics (sent, opened, replied counts)
- Webhooks handle real-time updates going forward
Setup:
- Get your API key from Smartlead Settings
- Add to
SMARTLEAD_API_KEYin.env.localor configure via Admin Settings UI
API Client (src/lib/smartlead/):
| File | Purpose |
|---|---|
client.ts |
Base HTTP client with query parameter auth (?api_key=...) |
campaigns.ts |
List and manage campaigns |
accounts.ts |
Fetch email accounts and warmup analytics |
analytics.ts |
Campaign performance data |
types.ts |
SmartleadAccount, SmartleadWarmupStats interfaces |
Key Smartlead API Endpoints Used:
GET /campaigns/{id}/analytics— Lightweight aggregate stats (sent, opened, replied, bounced)GET /campaigns/{id}/statistics?limit=100&offset=0— Per-lead engagement data withreply_time,sent_time,open_time,lead_categoryGET /campaigns/{id}/leads?limit=100&offset=0— Lead list with status and categoryGET /campaigns/{id}/leads/{id}/message-history— Email thread for a specific lead
Lead Sync with Statistics Enrichment:
During sync, fetchAllLeadsWithStats() merges the /leads and /statistics endpoints to:
- Set
is_positive_replyfromlead_category(Interested, Meeting Request) - Track
emailReplyCountfromreply_timepresence - Populate
responded_atfrom the Smartleadreply_timetimestamp
Setup: Configure HubSpot API key per client via the client settings.
Client (src/lib/hubspot/):
| File | Purpose |
|---|---|
client.ts |
HubSpot API HTTP client |
sync.ts |
Lead → HubSpot contact sync logic |
types.ts |
HubSpot type definitions |
The src/lib/providers/ directory provides a unified interface across providers:
src/lib/providers/
├── types.ts # Shared interfaces: ProviderLead, ProviderCampaignAnalytics, etc.
├── index.ts # Provider factory (createProvider, getProviderForCampaign)
├── instantly/
│ ├── client.ts # Instantly HTTP client (Bearer auth)
│ └── index.ts # Instantly provider implementation
└── smartlead/
├── client.ts # Smartlead HTTP client (query param auth)
└── index.ts # Smartlead provider implementation
ProviderLead Interface:
Normalized lead data shared across providers. Key fields:
id,email,firstName,lastName,companyName,companyDomainstatus,interestStatus(interested, not_interested, meeting_booked, etc.)emailOpenCount,emailClickCount,emailReplyCountrepliedAt— Reply timestamp from provider (e.g., Smartleadreply_time)rawData— Full provider-specific data for export
Endpoint: POST /api/webhooks/instantly/[campaignId]
Configure in Instantly to send events to this URL for each campaign.
Events Handled:
| Event | Action |
|---|---|
lead_interested |
Set is_positive_reply = true, send notification email |
lead_not_interested |
Set is_positive_reply = false |
email_sent |
Increment emails_sent counter |
email_opened |
Increment open count |
email_replied |
Update has_replied, replied_at |
lead_created |
Create new lead record |
link_clicked |
Increment click count |
meeting_booked |
Update lead status to booked |
lead_won |
Update lead status to won |
lead_lost |
Update lead status to lost |
Security: Webhook payloads are validated using INSTANTLY_WEBHOOK_SECRET.
Endpoint: POST /api/webhooks/smartlead/[campaignId]
Handles similar events from Smartlead campaigns (EMAIL_SENT, EMAIL_REPLY, LEAD_CATEGORY_UPDATED, etc.).
Endpoint: POST /api/webhooks/[provider]/[campaignId]
Generic webhook handler that routes to the correct provider implementation based on the [provider] path parameter (instantly or smartlead).
Endpoint: POST /api/webhooks/stripe
Events Handled:
| Event | Action |
|---|---|
checkout.session.completed |
Create subscription record in database |
customer.subscription.updated |
Sync plan/status changes |
customer.subscription.deleted |
Mark subscription as canceled |
invoice.payment_failed |
Update status to past_due |
invoice.payment_succeeded |
Update status to active |
Scheduled endpoints for automated data syncing. All cron endpoints require ?secret=CRON_SECRET for authentication.
| Endpoint | Purpose | Recommended Schedule |
|---|---|---|
GET /api/cron/smartlead-stats?secret=... |
Sync Smartlead campaign stats (lightweight: 2 API calls per campaign) | Every 4 hours |
GET /api/cron/analytics-snapshot?secret=... |
Create daily analytics snapshot for Instantly campaigns | 0 6 * * * (daily 6 AM UTC) |
GET /api/cron/smartlead-weekly?secret=... |
Weekly Smartlead daily analytics backfill | 0 6 * * 1 (Monday 6 AM UTC) |
GET /api/cron/stats-report?secret=... |
Send weekly/monthly stats report emails to configured recipients | 0 9 * * 1 (Monday 9 AM UTC) |
GET /api/cron/backfill-reply-dates?secret=... |
One-time backfill of responded_at from Smartlead reply_time |
Run manually as needed |
Lightweight cron that syncs cached dashboard stats for all Smartlead campaigns:
- Fetches campaign analytics (2 API calls per campaign:
/analytics+/campaigns/{id}) - Updates
cached_emails_sent,cached_reply_count,cached_emails_bounced,cached_leads_count,cached_contacted_count - Compares provider reply count vs local DB reply count, uses the higher value
- Supports
?campaign_id=UUIDparam to sync a single campaign - 200ms delay between campaigns for rate limiting
One-time utility to backfill responded_at timestamps for Smartlead leads:
- Paginates through Smartlead
/statisticsendpoint for each campaign - Extracts
reply_timeper lead and updatesresponded_atin Supabase - Only updates leads where
responded_atis currently NULL - Supports
?campaign_id=UUIDparam for single-campaign backfill
Sends HTML email reports to configured notification recipients:
- Uses
campaign_analytics_dailyfor date-filtered data (weekly/monthly periods) - Falls back to cached campaign stats when daily analytics data is unavailable or all zeros (common for Smartlead campaigns)
- Recipients configured per client in notification settings
cron-job.org (recommended for Railway/Docker deployments):
- Create a free account at cron-job.org
- Add jobs pointing to
https://your-domain.com/api/cron/{endpoint}?secret=YOUR_CRON_SECRET - Set schedule (e.g., every 4 hours for smartlead-stats, weekly for stats-report)
Vercel Cron (for Vercel deployments):
// vercel.json
{
"crons": [
{ "path": "/api/cron/smartlead-stats?secret=...", "schedule": "0 */4 * * *" },
{ "path": "/api/cron/analytics-snapshot?secret=...", "schedule": "0 6 * * *" },
{ "path": "/api/cron/stats-report?secret=...", "schedule": "0 9 * * 1" }
]
}GitHub Actions / Supabase pg_cron: See CRON_SETUP.md for additional setup options.
Monitor email account health across all providers with DNS validation. Accessible at /admin/infrastructure. All data scoped to the agency owner's email accounts.
- Account Dashboard: Total accounts, active count, average reputation, domain count
- Account Table: Filterable by provider, status, client assignment
- Client Assignment: Map email accounts to specific clients
- DNS Health: SPF, DKIM, DMARC validation for all sending domains
- Health History: Daily snapshots for tracking reputation trends
- Auto-refresh: Dashboard updates every 30 seconds
Uses DNS-over-HTTPS (Google DoH API) for server-side DNS lookups:
- SPF: Validates
v=spf1records for sending authorization - DKIM: Probes common selectors (google, default, selector1, selector2, k1, etc.)
- DMARC: Checks
_dmarcTXT records and parses policy (none,quarantine,reject) - Health Score: Generated 0–100 score based on DNS record completeness and validity
Built with React Email and sent via Resend.
| Template | Trigger | Description |
|---|---|---|
Invitation.tsx |
Admin invites client user | Welcome email with login link |
PositiveReplyNotification.tsx |
Webhook: positive reply | Alert with "Reply to Lead" and "View in Dashboard" buttons |
StatsReport.tsx |
Weekly cron job | Summary of campaign performance |
When a positive reply notification is sent, the "Reply to Lead" button uses a reply token system:
- Server creates a short-lived token storing the lead's email, subject, and quoted thread
- Notification email links to
/reply?token=... - First visit: user chooses Gmail or Outlook
- Preference saved to localStorage for auto-redirect on future clicks
- Tokens expire after 72 hours
| Plan | Price | Client Limit | Features |
|---|---|---|---|
| Starter | $49/mo | 3 clients | All features |
| Growth | $99/mo | 10 clients | All features |
| Agency | $249/mo | Unlimited | All features |
All plans include a 14-day free trial with no credit card required upfront.
- New agency owner signs up → redirected to
/choose-plan - Selects a plan → Stripe Checkout with 14-day trial
- Subscription synced to
stripe_subscriptionstable via webhook - Client creation enforced against plan limits
- Trial/past-due banners shown in admin layout
- "Manage Billing" in
/admin/billingopens Stripe customer portal
Platform super admins (configured in src/lib/stripe/helpers.ts) get free unlimited access without needing a subscription.
Every agency owner's data is fully isolated:
clients.owner_id— each client belongs to exactly one agencysettings.owner_id— API keys, branding per agencyemail_accounts.owner_id— infrastructure per agencysubscriptions.owner_id— credit tracking per agencylead_sources.owner_id— lead database per agency- Campaigns, leads, analytics all chain through
client_id → clients.owner_id
The getEffectiveOwnerId() helper ensures every API route and server component filters by the correct owner, with no unscoped code paths.
Platform admins can view any agency's workspace exactly as the agency owner sees it:
- Go to Settings → Agency Owners
- Click "View Dashboard" on any agency
- An amber banner appears: "Viewing as: Agency Name (email) · Stop Viewing"
- All data (clients, campaigns, leads, infrastructure, settings) is scoped to that agency
- Click "Stop Viewing" to return to the platform admin's own dashboard
Implementation uses an httpOnly cookie (x-view-as-owner) with 8-hour expiry, read by getEffectiveOwnerId() in every API route.
Agency Lead Operations SaaS includes comprehensive SEO out of the box for all marketing pages.
Every marketing page exports its own metadata object with:
- Unique, keyword-targeted
titleanddescription - OpenGraph tags (title, description, image, URL)
- Twitter card tags (summary_large_image)
- Canonical URLs via
alternates.canonical
The root layout (layout.tsx) defines:
metadataBasefor resolving relative URLstitle.template(%s | Agency Lead Operations) for consistent page titles- 12 long-tail keywords targeting outbound agency search terms
- Robots directives allowing Google to index with max image/snippet previews
| File | Route | Purpose |
|---|---|---|
src/app/sitemap.ts |
/sitemap.xml |
Lists all public pages with priorities and change frequencies |
src/app/robots.ts |
/robots.txt |
Allows /, blocks /admin/, /dashboard/, /api/, /auth/ |
Homepage includes a @graph with three schemas:
Organization— name, logo, descriptionSoftwareApplication— category, feature list, aggregate pricing ($49–$249), aggregate ratingWebSite— enables sitelinks search box eligibility
Pricing page includes:
FAQPageschema generated from the FAQ section — eligible for Google rich snippet FAQ accordion
Primary terms the pages are optimized for:
- "client reporting dashboard"
- "outbound agency software"
- "instantly dashboard" / "smartlead dashboard"
- "white-label client portal"
- "agency reporting tool"
- "cold email reporting"
- "campaign analytics dashboard"
- "lead generation agency software"
# Build the image
docker build -t agency-lead-ops-saas .
# Run with Docker Compose
docker-compose up -ddocker-compose.yml includes:
appservice: Next.js application (internal, not port-exposed)caddyservice: Reverse proxy with automatic HTTPS on ports 80/443- Shared
webbridge network between services
services:
app:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
environment:
- NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}
- INSTANTLY_WEBHOOK_SECRET=${INSTANTLY_WEBHOOK_SECRET}
- CRON_SECRET=${CRON_SECRET}
networks:
- web
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
depends_on:
- app
networks:
web:
driver: bridge
volumes:
caddy_data:
caddy_config:- Connect your GitHub repository to Railway
- Add all environment variables in the Railway dashboard (including
CRON_SECRET) - Railway auto-deploys on push to
main - Set up external cron jobs via cron-job.org (see Cron Jobs)
- Set up Stripe webhook endpoint pointing to your Railway URL
- Connect your GitHub repository to Vercel
- Add all environment variables in the Vercel dashboard
- Deploy
- Configure cron jobs in
vercel.json(see Cron Jobs) - Set up Stripe webhook endpoint pointing to your Vercel URL
| Environment | Database | Providers | Billing | |
|---|---|---|---|---|
| Development | Supabase Dev Project | Instantly/Smartlead Sandbox | Stripe Test Mode | Resend Test |
| Staging | Supabase Staging Project | Instantly/Smartlead Sandbox | Stripe Test Mode | Resend Test |
| Production | Supabase Production Project | Instantly/Smartlead Production | Stripe Live Mode | Resend Production |
src/
├── app/
│ ├── (marketing)/ # Marketing pages (grouped route)
│ │ ├── features/page.tsx # Features page
│ │ ├── pricing/page.tsx # Pricing page
│ │ └── layout.tsx # Marketing layout
│ │
│ ├── admin/ # Admin portal
│ │ ├── page.tsx # Command center dashboard
│ │ ├── layout.tsx # Admin layout (header, banners)
│ │ ├── billing/ # Subscription billing
│ │ │ ├── page.tsx # Billing overview
│ │ │ └── billing-actions.tsx # Manage billing button
│ │ ├── clients/ # Client management
│ │ │ └── [clientId]/ # Individual client
│ │ │ ├── page.tsx # Client dashboard + lead workflow
│ │ │ ├── settings/page.tsx # Client settings
│ │ │ └── campaigns/ # Campaign management
│ │ │ ├── page.tsx # Campaign list
│ │ │ └── [campaignId]/page.tsx # Campaign details
│ │ ├── leads/ # Lead management
│ │ │ ├── page.tsx
│ │ │ └── admin-leads-view.tsx
│ │ ├── lead-database/page.tsx # Centralized lead database
│ │ ├── instantly/ # Instantly integration UI
│ │ │ ├── page.tsx # Overview
│ │ │ ├── accounts/page.tsx # Email accounts
│ │ │ ├── campaigns/page.tsx # Campaign management
│ │ │ └── analytics/page.tsx # Provider analytics
│ │ ├── infrastructure/ # Infrastructure health
│ │ │ ├── page.tsx
│ │ │ └── infrastructure-view.tsx
│ │ ├── ai-campaigns/page.tsx # AI campaign builder (5-step wizard)
│ │ ├── scraping/page.tsx # Lead scraping (coming soon)
│ │ ├── notes/page.tsx # Internal notes
│ │ ├── subscriptions/page.tsx # Subscription tracking
│ │ └── settings/page.tsx # Agency settings + agency owners list
│ │
│ ├── api/ # API routes
│ │ ├── admin/ # Admin-only endpoints
│ │ │ ├── agencies/route.ts # List agency owners (platform admin)
│ │ │ ├── analytics/ # Analytics + sync
│ │ │ ├── customers/ # Client CRUD
│ │ │ ├── impersonate/route.ts # Impersonation start/stop
│ │ │ ├── infrastructure/ # Infrastructure health
│ │ │ ├── invitations/ # Client invitations
│ │ │ ├── lead-database/ # Lead database queries
│ │ │ ├── leads/ # Lead export
│ │ │ ├── notes/ # Notes CRUD
│ │ │ ├── notification-preferences/
│ │ │ ├── settings/ # App settings + logo
│ │ │ └── subscriptions/ # Subscription info
│ │ ├── stripe/ # Stripe billing endpoints
│ │ │ ├── checkout/route.ts # Create checkout session
│ │ │ ├── portal/route.ts # Create billing portal session
│ │ │ └── subscription/route.ts # Get subscription status
│ │ ├── reply-tokens/ # Smart reply token system
│ │ │ ├── route.ts # Create token
│ │ │ └── [token]/route.ts # Fetch token data
│ │ ├── campaigns/[campaignId]/ # Campaign operations
│ │ │ ├── details/ # Stats + details
│ │ │ ├── leads/ # Campaign leads
│ │ │ ├── variant-analytics/ # A/B test results
│ │ │ ├── sync-leads/ # Sync from provider
│ │ │ ├── sync-emails/ # Sync email threads
│ │ │ ├── export-leads/ # CSV export
│ │ │ ├── diagnose/ # Campaign diagnostics
│ │ │ ├── recalculate/ # Recalculate stats
│ │ │ └── generate-skill/ # Skill report
│ │ ├── clients/[clientId]/ # Client-specific endpoints
│ │ │ ├── campaigns/ # Client campaigns
│ │ │ ├── leads/ # Client leads
│ │ │ ├── invitations/ # Manage invitations
│ │ │ ├── hubspot/ # HubSpot sync
│ │ │ ├── logo/ # Logo upload
│ │ │ ├── stats-settings/ # Stats display config
│ │ │ ├── notification-preferences/
│ │ │ ├── sync-positive/ # Sync positive replies
│ │ │ └── tool-links/ # External tool links
│ │ ├── providers/[provider]/ # Multi-provider endpoints
│ │ │ ├── campaigns/
│ │ │ └── validate/
│ │ ├── instantly/ # Instantly-specific endpoints
│ │ ├── smartlead/ # Smartlead-specific endpoints
│ │ ├── leads/[leadId]/ # Lead operations
│ │ │ └── workflow/ # Workflow updates
│ │ ├── webhooks/ # Webhook handlers
│ │ │ ├── [provider]/[campaignId]/ # Unified provider webhooks
│ │ │ ├── instantly/[campaignId]/ # Instantly webhooks
│ │ │ ├── smartlead/[campaignId]/ # Smartlead webhooks
│ │ │ └── stripe/route.ts # Stripe webhooks
│ │ ├── cron/ # Scheduled jobs
│ │ │ ├── analytics-snapshot/ # Daily Instantly analytics snapshot
│ │ │ ├── smartlead-stats/ # Smartlead cached stats sync (every 4h)
│ │ │ ├── smartlead-weekly/ # Weekly Smartlead daily analytics backfill
│ │ │ ├── stats-report/ # Weekly/monthly stats email reports
│ │ │ └── backfill-reply-dates/ # One-time reply date backfill from Smartlead
│ │ └── demo/ # Demo utilities
│ │
│ ├── auth/ # Auth routes
│ │ ├── callback/ # OAuth callback handler
│ │ ├── signout/ # Sign out
│ │ └── accept-invite/ # Accept client invitation
│ │
│ ├── choose-plan/page.tsx # Post-signup plan selection
│ ├── reply/page.tsx # Smart email reply redirect (Gmail/Outlook)
│ │
│ ├── login/ # Login page
│ │ ├── page.tsx
│ │ └── login-client.tsx # Context-aware login (agency vs client)
│ ├── access-denied/page.tsx # Access denied page
│ ├── page.tsx # Landing page (testimonials with photos)
│ ├── layout.tsx # Root layout (SEO metadata)
│ ├── sitemap.ts # Auto-generated /sitemap.xml
│ ├── robots.ts # Auto-generated /robots.txt
│ └── globals.css # Global styles
│
├── components/
│ ├── admin/ # Admin-specific components
│ │ ├── add-customer-dialog.tsx
│ │ ├── delete-customer-dialog.tsx
│ │ └── impersonation-banner.tsx # Amber "Viewing as" banner
│ ├── campaigns/
│ │ └── variant-analytics.tsx # A/B variant display
│ ├── layout/
│ │ ├── header.tsx # Top navigation
│ │ ├── sidebar.tsx # Admin sidebar
│ │ └── stats-cards.tsx # Statistics display cards
│ ├── leads/
│ │ ├── EmailThread.tsx # Email thread viewer
│ │ ├── lead-detail-panel.tsx # Slide-out lead detail panel
│ │ └── lead-table.tsx # Lead listing table
│ ├── marketing/
│ │ ├── header.tsx # Marketing page header
│ │ └── footer.tsx # Marketing page footer
│ ├── ui/ # shadcn/ui components
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── info-tooltip.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── select.tsx
│ │ ├── sheet.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ └── textarea.tsx
│ └── theme-toggle.tsx # Dark/light theme toggle
│
├── hooks/
│ └── useVisibilityPolling.ts # Auto-refresh hook (30s polling)
│
├── lib/
│ ├── auth.ts # Auth helpers (requireAdmin, getEffectiveOwnerId, etc.)
│ ├── impersonation.ts # Impersonation cookie helpers (set/clear/get)
│ ├── utils.ts # General utilities (cn, etc.)
│ ├── supabase/ # Supabase clients
│ │ ├── client.ts # Browser client
│ │ ├── server.ts # Server client
│ │ └── middleware.ts # Auth middleware
│ ├── stripe/ # Stripe billing
│ │ ├── client.ts # Stripe instance singleton
│ │ ├── config.ts # Plan definitions (Starter/Growth/Agency)
│ │ └── helpers.ts # isSuperAdmin(), getUserSubscription()
│ ├── queries/ # Database query functions
│ │ ├── analytics.ts
│ │ ├── campaigns.ts
│ │ ├── clients.ts
│ │ ├── leads.ts
│ │ └── stats.ts
│ ├── instantly/ # Instantly API client
│ │ ├── client.ts # Base HTTP client (Bearer auth)
│ │ ├── campaigns.ts # Campaign operations
│ │ ├── leads.ts # Lead operations
│ │ ├── emails.ts # Email thread fetching
│ │ ├── analytics.ts # Analytics queries
│ │ ├── accounts.ts # Email account management
│ │ ├── types.ts # Type definitions
│ │ └── index.ts # Module exports
│ ├── smartlead/ # Smartlead API client
│ │ ├── client.ts # Base HTTP client (query param auth)
│ │ ├── campaigns.ts # Campaign operations
│ │ ├── accounts.ts # Account + warmup data
│ │ ├── analytics.ts # Analytics queries
│ │ ├── types.ts # Type definitions
│ │ └── index.ts # Module exports
│ ├── providers/ # Multi-provider abstraction
│ │ ├── types.ts # Shared provider interface
│ │ ├── index.ts # Provider factory
│ │ ├── instantly/ # Instantly adapter
│ │ └── smartlead/ # Smartlead adapter
│ ├── hubspot/ # HubSpot CRM integration
│ │ ├── client.ts # HubSpot API client
│ │ ├── sync.ts # Lead → contact sync
│ │ ├── types.ts # Type definitions
│ │ └── index.ts # Module exports
│ ├── dns/ # DNS health checker
│ │ ├── checker.ts # SPF/DKIM/DMARC validation via Google DoH
│ │ └── index.ts # Module exports
│ └── email/ # Email notification service
│ ├── index.ts # Module exports
│ ├── send.ts # Resend send wrapper + reply URL builders
│ └── templates/ # React Email templates
│ ├── Invitation.tsx # Client invitation email
│ ├── PositiveReplyNotification.tsx # Positive reply alert
│ └── StatsReport.tsx # Weekly stats summary
│
├── types/
│ └── database.ts # TypeScript types for all tables
│
└── middleware.ts # Next.js middleware (auth + role checks)
├── .env.example # Environment variable template
├── .gitignore # Git exclusions
├── .dockerignore # Docker exclusions
├── Caddyfile # Caddy reverse proxy config
├── CLAUDE.md # Claude AI project context
├── CRON_SETUP.md # Cron job setup guide
├── README.md # This file
├── components.json # shadcn/ui config
├── docker-compose.yml # Docker multi-service config
├── Dockerfile # Container build config
├── eslint.config.mjs # ESLint configuration
├── next.config.ts # Next.js configuration
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
├── supabase-schema.sql # Migration 1: Core schema
├── supabase-schema-v2.sql # Migration 2: Multi-provider + denormalization
├── supabase-settings.sql # Migration 3: Settings table
├── supabase-preserve-leads.sql # Migration 4: Lead preservation triggers
├── supabase-analytics.sql # Migration 5: Analytics views + functions
├── supabase-add-icp.sql # Migration 6: ICP fields
└── supabase/migrations/ # Numbered migrations
├── 20260303_add_reply_tokens.sql
├── 20260304_add_stripe_billing.sql
├── 20260304_add_multi_tenancy.sql
└── 20260305_add_owner_scoping.sql
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
For questions, bug reports, or feature requests, please open an issue on GitHub.