Automation & Pipelines
Marketing & Lead Automation
Lead nurturing, campaigns, and marketing automation capabilities in ServiceFlow.
8 min readMarketing Portal — Standalone SaaS
HubSpot-style marketing platform that can run independently or integrate with ServiceFlow Pro via the ServiceFlow bridge.
Architecture
| Component | Package / app | Role |
|---|---|---|
| Shared types & validation | @serviceflow/marketing-core | Campaign steps, Zod schemas, trigger event names |
| Database | @serviceflow/marketing-db | Portable Postgres schema (marketing_* tables only) |
| Business logic | @serviceflow/marketing-domain | Services with interfaces (accounts, campaigns, contacts, execution, auth) |
| HTTP API | @serviceflow/marketing-api | Standalone Express API (/v1/*) |
| UI | @serviceflow/marketing-portal | Vite + React + Ant Design |
| ServiceFlow bridge | @serviceflow/marketing-serviceflow-bridge | Maps businessId → marketing tenant, syncs customers, forwards events |
Database (shared with ServiceFlow by default)
Marketing uses
marketing_* tables in the same Postgres database as ServiceFlow — no second server required for local dev or production.| Variable | When to use |
|---|---|
DATABASE_URL | Default. Same value as serviceflow-api (monorepo root .env). |
MARKETING_DATABASE_URL | Optional. Dedicated Postgres when you split marketing to its own host (reseller / standalone). Overrides DATABASE_URL for marketing-api and packages/marketing-db migrations only. |
Migrations are portable: ship
packages/marketing-db/prisma/migrations to a buyer and point either variable at their Postgres.Quick start (ServiceFlow monorepo)
cd HomeImprovment
pnpm install
# 1. Root .env already has DATABASE_URL for the API — marketing uses the same URL
# (marketing-api also loads ../../.env automatically)
# 2. Marketing-only secrets (optional file; can live in root .env too)
cp apps/marketing-api/.env.example apps/marketing-api/.env
# MARKETING_JWT_SECRET, MARKETING_SERVICE_API_KEY, Mailgun keys
# Do not put DATABASE_URL in apps/marketing-api/.env unless using MARKETING_DATABASE_URL for a split DB.
# 3. Migrate ServiceFlow + marketing tables on the shared database
pnpm db:migrate:all
pnpm db:generate:marketing
# 4. Run API + portal
pnpm marketing
# Or with the full stack:
pnpm dev
Quick start (standalone / reseller — separate database)
# Point at a buyer-owned Postgres (or a second DB on the same server)
export MARKETING_DATABASE_URL=postgresql://user:pass@host:5432/marketing
pnpm db:generate:marketing && pnpm db:migrate:marketing
pnpm marketing
- Portal: http://localhost:5178
- API: http://localhost:4002
- Register a workspace, add contacts, create drip campaigns.
Dev commands
| Command | What starts |
|---|---|
pnpm dev | All monorepo apps including marketing-api (4002) and marketing-portal (5178) |
pnpm marketing | Marketing API + portal only |
pnpm db:migrate | ServiceFlow schema (packages/database) |
pnpm db:migrate:marketing | Marketing marketing_* tables (same DATABASE_URL by default) |
pnpm db:migrate:all | Both migrations on the shared database |
pnpm db:generate:marketing | Regenerate Prisma client for marketing-db |
marketing-api loads the monorepo root .env first, then apps/marketing-api/.env for overrides.Production deploy (Cloud Run)
- Create secrets (one time):
./scripts/setup-marketing-secrets.sh - Apply marketing migrations on the shared DB:
pnpm db:migrate:marketing - Deploy:
./scripts/deploy-scripts/marketing-api.deploy.sh./scripts/deploy-scripts/marketing-portal.deploy.sh./scripts/deploy-scripts/api.deploy.sh(setsMARKETING_API_URL+ service key)./scripts/deploy-scripts/web.deploy.sh(bakesVITE_MARKETING_PORTAL_URL)
Map custom domains (prod examples):
marketing-api.serviceflow-pro.com, marketing.serviceflow-pro.com.ServiceFlow integration
When ServiceFlow should use the standalone API instead of in-process marketing services:
# apps/api/.env
MARKETING_API_URL=http://localhost:4002
MARKETING_SERVICE_API_KEY=<same as marketing-api MARKETING_SERVICE_API_KEY>
If workspace setup fails with a missing column (e.g. `workspaceId`), the shared DB still has legacy marketing tables. Run:
```bash
pnpm db:migrate:marketing
```env
# apps/web/.env
VITE_MARKETING_PORTAL_URL=http://localhost:5178
business-marketingroutes proxy tomarketing-api/v1/service/*(exceptsync-and-enrolland AI email generation, which stay in ServiceFlow).- Settings → Marketing Campaigns embeds the portal iframe when
VITE_MARKETING_PORTAL_URLis set.
Environment variables
| Variable | App | Description |
|---|---|---|
DATABASE_URL | marketing-db / marketing-api | Postgres connection (buyer-owned DB) |
MARKETING_JWT_SECRET | marketing-api | JWT signing for portal users |
MARKETING_SERVICE_API_KEY | marketing-api, ServiceFlow API | Service-to-service auth |
MARKETING_API_URL | ServiceFlow API | Base URL for proxy/bridge |
MARKETING_API_PORT | marketing-api | Listen port (default 4002) |
MARKETING_API_PUBLIC_URL | marketing-api | Public URL for tracking pixels |
MAILGUN_API_KEY | marketing-api | System Mailgun key |
MAILGUN_DOMAIN | marketing-api | System Mailgun domain |
MAILGUN_WEBHOOK_SIGNING_KEY | marketing-api | Webhook verification |
MARKETING_CORS_ORIGIN | marketing-api | Portal origin(s); e.g. http://localhost:5178,http://localhost:5173 |
VITE_MARKETING_API_URL | marketing-portal | API base (default http://localhost:4002/v1) |
VITE_MARKETING_PORTAL_URL | ServiceFlow web | Embed URL for settings page |
HubSpot parity roadmap
Phase 1 — Shipped in this extraction
- Workspaces + JWT auth
- Contacts CRUD
- Campaigns (drip, one-time, triggered types)
- Step builder (email, SMS, task, wait)
- Templates (list; full CRUD via JWT routes)
- Enrollments + analytics
- Mailgun send + open/click tracking webhooks
- Cron execution (every 15 min)
- ServiceFlow proxy + customer sync API
Phase 2 — CRM core (implemented)
- Companies — CRUD, contact linking via
companyId - Lists — static lists, bulk add/remove members
- Custom properties — definitions per
contact/companyobject type - Contact detail — edit, company dropdown, custom fields, activity timeline, campaign enroll
- Contact import —
POST /v1/contacts/import - ServiceFlow sync — customer sync upserts companies and links
companyId;POST /v1/service/companies/syncfor bulk company import
Portal routes:
/contacts, /companies, /lists, /settings/propertiesJWT API (portal):
GET/POST /v1/companies,GET/PUT/DELETE /v1/companies/:companyIdGET/POST /v1/lists,GET/PUT/DELETE /v1/lists/:listIdPOST/DELETE /v1/lists/:listId/members,GET /v1/lists/:listId/contactsGET/POST /v1/property-definitions,PUT/DELETE /v1/property-definitions/:idGET/PUT/DELETE /v1/contacts/:contactId,GET/POST /v1/contacts/:contactId/activitiesPOST /v1/contacts/import
Service API (ServiceFlow proxy): mirrored under
/v1/service/* (companies, lists, contacts, activities)Phase 3a — Email & domains (shipped)
- Portal Settings → Email & Domains (
/settings/email) GET/PUT /v1/settings/email, verify domain, test send- ServiceFlow Mailgun sync on workspace provision (
POST /v1/service/email-settings/sync) - Auto Mailgun domain + webhook provision on save (
POST /v1/settings/email/provision,POST /v1/service/email-settings/provision) - Mailgun events relay: ServiceFlow
/api/webhooks/mailgun/events→ marketing-api/v1/service/mailgun/webhook - Public unsubscribe (
GET/POST /v1/public/unsubscribe) with signed links in campaign emails - Mailgun webhooks:
delivered,complained,unsubscribed,permanent_fail
Phase 3b — Segments, triggers, editor, SMS, analytics (shipped)
- Dynamic lists —
filterRulesJSON, live-queryMarketingSegmentService, portalSegmentFilterBuilder,GET /lists/:id/preview-count - Triggered campaigns — Portal trigger event/conditions/segments;
MarketingTriggerServiceresolves contacts and evaluates rules - ServiceFlow bridge —
marketingEventBridgeServiceforwards job/customer/auth/subscription events to marketing-api whenMARKETING_API_URLis set (legacy fallback otherwise) - Rich email editor —
react-quillin campaign email steps - SMS steps —
ServiceFlowSmsProvider,marketing_sent_sms, sync viaPOST /v1/service/sms-settings/sync - Analytics — Click link wrapping, bounced/unsubscribed/SMS counts in portal analytics modal
Phase 3c — Enterprise readiness (shipped)
- DB migration
20260603120000_enterprise_marketing_features— suppressions, email events/clicks, consent, audit log, job queue, SMS lifecycle columns - Send hardening — EU Mailgun region, reply-to on sends, webhook signature required in production, enrollment counter maintenance, failed sends do not advance steps
- Job queue —
marketing_job_queue+ cron processor (replaces direct in-process campaign execution) - Tracking APIs —
GET /campaigns/:id/messages,GET /campaigns/:id/time-series, detailed enrollments (?detailed=true) - SMS lifecycle — Twilio/Telnyx status webhooks, STOP/HELP inbound handler, portal SMS settings + test send
- Suppressions —
GET/POST/DELETE /v1/suppressions, checked before send - Portal UX — Message log, time-series chart, quota banner, CSV import, bulk/list enroll, pause/resume campaigns and enrollments, template pre-fill
Phase 3 — Deals (schema ready)
Tables:
UI: Kanban pipeline board
marketing_pipelines, marketing_deal_stages, marketing_dealsUI: Kanban pipeline board
Phase 4 — Marketing Hub
- Forms + submissions (
marketing_forms— add in Phase 4 migration) - Landing pages / embed script
- UTM attribution on contacts
Phase 5 — Visual workflows
- Graph-based
workflow_nodes/workflow_edges - React Flow canvas
- Migrate linear
stepsJSON to workflows
Phase 6 — Reporting & reseller ops
- Dashboards (email, funnel, deal velocity)
- Multi-workspace agency model
- Quota/billing integration
Database contract for resellers
Ship only
packages/marketing-db/prisma/migrations to buyers. Set DATABASE_URL or MARKETING_DATABASE_URL on their Postgres. Schema has no foreign keys to ServiceFlow businesses or customers. Tenant linkage uses:marketing_accounts.connectorType—native|serviceflowmarketing_accounts.externalTenantId— buyer or ServiceFlow business idmarketing_contacts.metadata—sourceType,sourceId
API surface
Portal (JWT)
POST /v1/auth/register,POST /v1/auth/loginGET /v1/account/v1/campaigns,/v1/contacts,/v1/companies,/v1/lists,/v1/property-definitions,/v1/campaign-templates
ServiceFlow bridge (service key + X-External-Tenant-Id)
GET /v1/service/accountPOST /v1/service/contacts/syncPOST /v1/service/companies/syncPOST /v1/service/events- Mirrored business-marketing paths under
/v1/service/campaigns,/v1/service/contacts,/v1/service/companies,/v1/service/lists, etc.
Webhooks (public)
GET /v1/webhooks/track/open/:sentEmailIdGET /v1/webhooks/track/click/:sentEmailIdPOST /v1/webhooks/mailgun