Garage Door OS Docs
Automation & Pipelines

Marketing & Lead Automation

Lead nurturing, campaigns, and marketing automation capabilities in ServiceFlow.
8 min read

Marketing Portal — Standalone SaaS

HubSpot-style marketing platform that can run independently or integrate with ServiceFlow Pro via the ServiceFlow bridge.

Architecture

ComponentPackage / appRole
Shared types & validation@serviceflow/marketing-coreCampaign steps, Zod schemas, trigger event names
Database@serviceflow/marketing-dbPortable Postgres schema (marketing_* tables only)
Business logic@serviceflow/marketing-domainServices with interfaces (accounts, campaigns, contacts, execution, auth)
HTTP API@serviceflow/marketing-apiStandalone Express API (/v1/*)
UI@serviceflow/marketing-portalVite + React + Ant Design
ServiceFlow bridge@serviceflow/marketing-serviceflow-bridgeMaps 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.
VariableWhen to use
DATABASE_URLDefault. Same value as serviceflow-api (monorepo root .env).
MARKETING_DATABASE_URLOptional. 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

Dev commands

CommandWhat starts
pnpm devAll monorepo apps including marketing-api (4002) and marketing-portal (5178)
pnpm marketingMarketing API + portal only
pnpm db:migrateServiceFlow schema (packages/database)
pnpm db:migrate:marketingMarketing marketing_* tables (same DATABASE_URL by default)
pnpm db:migrate:allBoth migrations on the shared database
pnpm db:generate:marketingRegenerate Prisma client for marketing-db
marketing-api loads the monorepo root .env first, then apps/marketing-api/.env for overrides.

Production deploy (Cloud Run)

  1. Create secrets (one time): ./scripts/setup-marketing-secrets.sh
  2. Apply marketing migrations on the shared DB: pnpm db:migrate:marketing
  3. Deploy:
    • ./scripts/deploy-scripts/marketing-api.deploy.sh
    • ./scripts/deploy-scripts/marketing-portal.deploy.sh
    • ./scripts/deploy-scripts/api.deploy.sh (sets MARKETING_API_URL + service key)
    • ./scripts/deploy-scripts/web.deploy.sh (bakes VITE_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-marketing routes proxy to marketing-api /v1/service/* (except sync-and-enroll and AI email generation, which stay in ServiceFlow).
  • Settings → Marketing Campaigns embeds the portal iframe when VITE_MARKETING_PORTAL_URL is set.

Environment variables

VariableAppDescription
DATABASE_URLmarketing-db / marketing-apiPostgres connection (buyer-owned DB)
MARKETING_JWT_SECRETmarketing-apiJWT signing for portal users
MARKETING_SERVICE_API_KEYmarketing-api, ServiceFlow APIService-to-service auth
MARKETING_API_URLServiceFlow APIBase URL for proxy/bridge
MARKETING_API_PORTmarketing-apiListen port (default 4002)
MARKETING_API_PUBLIC_URLmarketing-apiPublic URL for tracking pixels
MAILGUN_API_KEYmarketing-apiSystem Mailgun key
MAILGUN_DOMAINmarketing-apiSystem Mailgun domain
MAILGUN_WEBHOOK_SIGNING_KEYmarketing-apiWebhook verification
MARKETING_CORS_ORIGINmarketing-apiPortal origin(s); e.g. http://localhost:5178,http://localhost:5173
VITE_MARKETING_API_URLmarketing-portalAPI base (default http://localhost:4002/v1)
VITE_MARKETING_PORTAL_URLServiceFlow webEmbed 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 / company object type
  • Contact detail — edit, company dropdown, custom fields, activity timeline, campaign enroll
  • Contact importPOST /v1/contacts/import
  • ServiceFlow sync — customer sync upserts companies and links companyId; POST /v1/service/companies/sync for bulk company import
Portal routes: /contacts, /companies, /lists, /settings/properties
JWT API (portal):
  • GET/POST /v1/companies, GET/PUT/DELETE /v1/companies/:companyId
  • GET/POST /v1/lists, GET/PUT/DELETE /v1/lists/:listId
  • POST/DELETE /v1/lists/:listId/members, GET /v1/lists/:listId/contacts
  • GET/POST /v1/property-definitions, PUT/DELETE /v1/property-definitions/:id
  • GET/PUT/DELETE /v1/contacts/:contactId, GET/POST /v1/contacts/:contactId/activities
  • POST /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 listsfilterRules JSON, live-query MarketingSegmentService, portal SegmentFilterBuilder, GET /lists/:id/preview-count
  • Triggered campaigns — Portal trigger event/conditions/segments; MarketingTriggerService resolves contacts and evaluates rules
  • ServiceFlow bridgemarketingEventBridgeService forwards job/customer/auth/subscription events to marketing-api when MARKETING_API_URL is set (legacy fallback otherwise)
  • Rich email editorreact-quill in campaign email steps
  • SMS stepsServiceFlowSmsProvider, marketing_sent_sms, sync via POST /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 queuemarketing_job_queue + cron processor (replaces direct in-process campaign execution)
  • Tracking APIsGET /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
  • SuppressionsGET/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: marketing_pipelines, marketing_deal_stages, marketing_deals
UI: 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 steps JSON 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.connectorTypenative | serviceflow
  • marketing_accounts.externalTenantId — buyer or ServiceFlow business id
  • marketing_contacts.metadatasourceType, sourceId

API surface

Portal (JWT)

  • POST /v1/auth/register, POST /v1/auth/login
  • GET /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/account
  • POST /v1/service/contacts/sync
  • POST /v1/service/companies/sync
  • POST /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/:sentEmailId
  • GET /v1/webhooks/track/click/:sentEmailId
  • POST /v1/webhooks/mailgun