Skip to content

Repository files navigation

GTM Engine

SaaS for go-to-market execution: an AI content pipeline (short videos/clips), automated B2B email outreach, and AI-generated GTM strategy plans — all behind a human-in-the-loop approval workflow.

Full technical spec: see ARCHITECTURE.md.

Stack

  • backend/ — FastAPI (Python 3.11+), SQLAlchemy 2.x, SQLite by default (Postgres-ready via DATABASE_URL), JWT auth, Alembic migrations, APScheduler for scheduled publishing and reply polling.
  • frontend/ — Next.js 14 (App Router), TypeScript, Tailwind CSS. Talks to the API via NEXT_PUBLIC_API_URL, JWT stored in localStorage.
  • Storage — local disk by default (backend/storage/); swaps to Tigris/S3 automatically when S3 credentials are set. Storage is meant to be temporary: once content is published, only the platform URL needs to be kept.

White-label + client approval portal

Stage 2 layers agency-facing white-label branding and a client-facing approval portal on top of Stage 1's tenancy. Full contract: STAGE2_SPEC.md.

  • BrandingOrganization.branding and Workspace.branding are both {app_name?, logo_url?, accent_color?, from_name?, from_email?, subdomain?} JSON; workspace branding overrides org branding via one merge helper (app/services/branding_service.py) used everywhere branding is resolved — the authed app, the client portal, outbound email from_name/from_email, and the branded report. Edit it at Settings → Organization (PATCH /orgs/me, agency plan required). The frontend applies branding as CSS variables via a single BrandingProvider — no per-tenant builds.
  • Client approval portal — agencies invite clients per-workspace (Settings → Workspace → "Client access") via a magic link (POST /workspaces/{id}/client-access); the client never gets a login or sees the agency's org. The link exchanges for a short-lived, portal-scoped JWT (GET /portal/auth?token=) that only works against /portal/* routes — app JWTs are rejected there and portal JWTs are rejected on every normal app route (app/tenancy.py CurrentPortalClient vs CurrentOrg/CurrentWorkspace). The portal (/portal/review, /portal/published, /portal/report) is mobile-first with no sidebar: review queue with video preview + approve/reject + comments, a read-only published list, and the same monthly report the agency sees. Approve/reject reuse the existing content service functions — the portal only adds a ContentComment author trail (author_kind: agency|client), never new state logic.
  • Monthly reports — one aggregate function (app/services/report_service.py) powers the agency report page, the portal report, and a downloadable/printable GET /workspaces/{id}/report.html?month= — a self-contained branded HTML document (inline CSS, logo, accent color; print-to-PDF, no PDF library needed). month is YYYY-MM, defaulting to the current month.

Real publishing: YouTube OAuth + publish kits

Connect a real YouTube account from Settings → Workspace → "Connected accounts" to publish scheduled content automatically instead of pasting URLs by hand. Requires GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and OAUTH_REDIRECT_BASE (see env vars below) — without them the connect button surfaces a clear "not configured" error and every platform, including YouTube, falls back to the publish kit flow: an approved item with no connected publisher for its platform gets a copy-paste kit (GET /content/{id}/publish-kit → video URL, caption, hashtags, checklist) shown in both the content detail page and the client portal, and the existing "mark published" action closes the loop once you've posted it manually.

Deliverability guardrails

Real agency sending is gated behind guardrails in app/services/deliverability_service.py, all enforced inside the outreach send path (never optional at the call site):

  • Suppression — unsubscribed, bounced, or manually-suppressed emails are checked before every send and skipped, never silently dropped; POST /campaigns/{id}/send reports back {queued, skipped_suppressed, deferred_cap} so the agency can see what got held back.
  • Unsubscribe — every outbound outreach email carries a footer link (GET /public/unsubscribe/{token}, a signed token, no login required) injected at the email-provider layer so no send path can bypass it; clicking it adds a suppression entry and shows a confirmation page.
  • Volume ramp — per-workspace daily send cap ramps up with days-since-first-send (20/day for days 1–3, 50/day days 4–7, 100/day week 2, 200/day after that), capped by the plan's own limit. An unverified sending domain hard-caps at 10/day regardless of ramp stage.
  • Domain verificationPOST /workspaces/{id}/verify-domain checks SPF and DMARC records for the workspace's settings.sending_domain via dnspython and stores {spf_found, dmarc_found, verified, checked_at} back onto workspace.settings.domain_verification; the UI shows a verified/unverified badge next to the sending domain field.
  • Bounce handling — the existing IMAP reply-poll job also detects bounce notifications by subject/from heuristics and marks the contact bounced + suppressed.

Multi-workspace tenancy

The app is organized as Organization → Workspace. Every user's account has one Organization (created automatically at registration), and an Organization holds one or more Workspaces — isolated containers for content, campaigns, GTM plans, and schedule config. Use workspaces to separate products or client accounts within the same org; every domain request must carry an X-Workspace-Id header (the frontend attaches this automatically for the active workspace, see lib/workspace-context.tsx). See STAGE1_SPEC.md for the full tenancy model.

Plan tiers (backend/app/plans.py) gate workspace count and monthly content-generation volume:

Plan Max workspaces Generations / month Client portal White-label
free (default) 1 10
starter 3 100 1 workspace (a taste)
agency 25 1000

Exceeding a limit returns HTTP 402 Payment Required with an upgrade message. GET /orgs/me reports current usage vs. limits; the frontend surfaces this in Settings → Organization. Client-portal invites and branding PATCH /orgs/me are gated the same way (starter's portal access is scoped to its oldest workspace only).

Billing runs in mock mode by default: POST /billing/checkout immediately upgrades the org's plan and returns {url: "/settings?upgraded=<plan>"} — no real payment happens, and it's logged clearly as mock. Set STRIPE_SECRET_KEY (plus STRIPE_WEBHOOK_SECRET and the STRIPE_PRICE_ID_* vars) to switch to real Stripe Checkout.

Quickstart

Run backend and frontend in two terminals.

1. Backend

cd backend
python3 -m venv venv
source venv/bin/activate        # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env            # edit if you want real providers; safe to leave blank
alembic upgrade head            # creates/updates the database schema
uvicorn app.main:app --reload

The API serves on http://localhost:8000. Interactive docs at http://localhost:8000/docs. Schema changes are managed by Alembic — run alembic upgrade head after every pull that touches app/models.py or alembic/versions/. (Tables are no longer created automatically on startup.)

2. Frontend

cd frontend
npm install
cp .env.example .env.local      # NEXT_PUBLIC_API_URL defaults to http://localhost:8000
npm run dev

The app serves on http://localhost:3000. Register a new account there to get started — there's no seed data.

Environment variables

backend/.env (see backend/.env.example)

Variable Purpose Required?
DATABASE_URL SQLAlchemy connection string No — defaults to local SQLite
JWT_SECRET, JWT_ALGORITHM, JWT_EXPIRE_MINUTES Auth token signing No — has dev defaults, change JWT_SECRET before any real deployment
ANTHROPIC_API_KEY / OPENAI_API_KEY Real LLM for scripts, email personalization, GTM plans, reply classification No — falls back to MockLLM
SMTP_HOST/PORT/USER/PASS, IMAP_HOST/USER/PASS Real email sending + reply polling No — falls back to MockEmail (simulated sends)
STORAGE_DIR Local asset storage path No — defaults to ./storage
AWS_ENDPOINT_URL_S3, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, BUCKET_NAME Tigris/S3-compatible object storage No — falls back to local disk
CORS_ORIGINS Allowed origins, comma-separated No — defaults to http://localhost:3000
FRONTEND_URL Used to build Stripe checkout redirect URLs No — defaults to http://localhost:3000
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID_STARTER, STRIPE_PRICE_ID_AGENCY Real billing via Stripe Checkout + webhook No — falls back to MockBilling (checkout instantly upgrades the org, no real payment)
SENTRY_DSN Error tracking No — Sentry only initializes when set
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, OAUTH_REDIRECT_BASE YouTube OAuth (connect account, resumable upload, token refresh) No — without these, "Connect YouTube" surfaces a clear "not configured" error and content falls back to the publish-kit flow
PORTAL_JWT_EXPIRE_MINUTES Client-portal session length after a magic-link exchange No — defaults to 24 hours

frontend/.env.local (see frontend/.env.example)

Variable Purpose Required?
NEXT_PUBLIC_API_URL Base URL of the backend API No — defaults to http://localhost:8000

Mock mode (runs with zero API keys)

Every external dependency sits behind a provider interface with a deterministic mock fallback, so the entire app — content generation, video assembly, email outreach, GTM plans — works fully offline with no keys configured:

  • LLM: no ANTHROPIC_API_KEY/OPENAI_API_KEYMockLLM returns deterministic templated scripts, email copy, GTM plans, and reply classifications.
  • TTS: no network/gTTSMockTTS writes a silent WAV of the correct duration.
  • Video: no moviepy/ffmpeg → MockAssembler writes a placeholder .mp4.
  • Email: no SMTP credentials → MockEmail records sends in the database with status simulated (nothing actually goes out; reply polling is disabled).
  • Publishing: the manual publisher is always available — you post the content yourself and paste the live URL back in in the UI. OAuth publishers (YouTube/TikTok/Meta) are stubs that raise a clear "not configured" error until built out.
  • Storage: no S3/Tigris credentials → local disk under backend/storage/.

Check what's mocked vs. live at any time via GET /health/providers, surfaced on the /settings page.

Nothing publishes automatically — every content item requires explicit approval (ScheduleConfig.require_approval, default true) before the scheduler will touch it.

Architecture

See ARCHITECTURE.md for the full spec: data models, API contract, provider pattern, GTM strategy engine, and scheduler behavior.

High-level shape:

  • backend/app/routers/ — one router per resource (auth, orgs, workspaces, billing, assets, content, schedule-config, campaigns, gtm, health, portal, public, social).
  • backend/app/services/ — query/mutation logic behind the routers (routers stay thin: parse → call service → return schema). Stage 2 adds branding_service.py (the one merge helper), report_service.py (the one aggregate, three surfaces), deliverability_service.py (suppression/ramp/domain checks), and social_service.py (YouTube OAuth flow).
  • backend/app/tenancy.pyCurrentOrg/CurrentWorkspace request-scoping dependencies used by every domain router; CurrentPortalClient is the equivalent for portal-scoped JWTs, kept fully separate so app and portal auth can never cross. get_workspace_for_app_or_portal covers the handful of endpoints (asset file serving) rendered as plain <img>/<video> src that need to accept either token type via a ?token= query-param fallback.
  • backend/app/providers/ — swappable provider implementations (LLM, TTS, video assembly, email, billing, publishing) with mock fallbacks. providers/publisher.py holds the manual/YouTube/TikTok/Meta publishers behind one interface.
  • backend/app/gtm/ — static playbook library + engine that tailors playbooks into a plan via the LLM provider.
  • backend/app/scheduler.py — APScheduler jobs: publish approved+due content every minute (falls back to the publish-kit flag when no publisher is connected), poll IMAP for replies + bounces every 5 minutes (only when configured).
  • backend/alembic/ — schema migrations; alembic upgrade head is required before first run (see Quickstart). Three revisions so far: 0001_initial_schema (Stage 1 tenancy), 0002_stage2 (branding, client portal, deliverability), and 0003_stage3 (portfolio metrics, audience graph, GTM-audit lead magnet, self-serve hardening).
  • frontend/lib/api.ts — single HTTP client module the authed app goes through (attaches JWT + X-Workspace-Id); frontend/lib/portal-api.ts is the equivalent for the client portal (separate token store, never mixed with the app token); frontend/lib/types.ts mirrors the backend models/schemas for both.

Deploying

Nothing is deployed by default — see deploy.md for the scaffolding to ship the backend to Fly.io (+ Fly Postgres, Tigris object storage) and the frontend to Vercel.

Roadmap

  • TikTok/Meta OAuth publishers — YouTube is the only platform with a real OAuth publisher so far; TikTok and Meta still raise a clear "not configured" error and fall back to the publish-kit flow (SocialAccount.oauth_token/refresh_token fields already support them).
  • Tigris/S3 asset lifecycle — automatic expiry/deletion of temporary generated assets once a ContentItem is published and only the platform URL is needed (Asset.expires_at is modeled but not yet enforced by a cleanup job).
  • Real Stripe billing in production — mock billing is the default; wire up live STRIPE_SECRET_KEY/webhook secrets per deploy.md when ready to charge real customers.
  • Reply-poll hardening — the IMAP reply poller does best-effort matching by sender email and subject/from bounce heuristics; needs threading (In-Reply-To/References), retry/backoff, and dedup before it's production-grade.
  • Team members — orgs are still single-owner; member invites/roles beyond owner are modeled (Membership.role) but there's no UI or endpoint to manage them yet.
  • PDF exportreport.html is designed to be printed to PDF from the browser; a server-side PDF renderer is deliberately out of scope for now.

Stage 3 features

Stage 3 layers portfolio-level intelligence and self-serve growth on top of Stages 1-2. Full contract: STAGE3_SPEC.md.

  • Org metricsGET /orgs/metrics rolls up weekly content/outreach/approval stats per workspace plus org-wide totals (app/services/metrics_service.py), with week-over-week deltas. Powers the dashboard's portfolio view across every workspace in an org at once.
  • Audience management + cross-promoAudienceContact is an org-wide contact graph (GET /audience, POST /audience/import for CSV upload, GET /audience/export.csv for download) fed by every workspace's outreach activity. POST /crosspromo drafts a content item + campaign to introduce one workspace's audience to another workspace in the same org — never sends anything automatically, always lands in the review queue first.
  • GTM audit tool — a public, unauthenticated lead magnet at POST /public/gtm-audit (rate-limited 3/day/IP) that generates a 90-day GTM plan from a product URL + category + channels, and returns a shareable link (GET /public/gtm-audit/{token}) rendering a self-contained HTML report — no login required to view. Submitting the audit form also captures a Lead record that converts into a real org on signup.
  • Onboarding templates — new users hit a first-run wizard (frontend/app/onboarding) that seeds a workspace from one of three templates (saas_product, client_smb, newsletter, defined in backend/app/gtm/templates.py) — each seeds a posting cadence, three starter content prompts, and a campaign skeleton in one POST /workspaces {template} call.
  • Self-serve authGET /auth/verify-email?token= confirms the signed link sent at registration; unverified orgs are capped at free-plan limits regardless of their actual billing plan (app/plans.py effective_plan_key) as an anti-abuse guard. POST /auth/forgot-password / POST /auth/reset-password provide a full self-serve password reset flow, non-leaking on unknown emails.

Project docs

Doc Covers
ARCHITECTURE.md Full technical spec — data models, API contract, provider pattern, GTM strategy engine, scheduler behavior
PRODUCT_ROADMAP.md Product-level roadmap and stage sequencing
STAGE1_SPEC.md Multi-workspace tenancy model (Organization → Workspace)
STAGE2_SPEC.md White-label branding, client approval portal, deliverability guardrails
STAGE3_SPEC.md Portfolio metrics, audience graph/cross-promo, GTM audit lead magnet, self-serve hardening
deploy.md Fly.io + Vercel deployment scaffolding, plus a pre-launch Production Checklist

Known gaps / next steps

Beyond the Roadmap items above, these are explicitly out of scope for the current build and worth flagging before relying on them:

  • No resend-verify-email endpoint — if a user misses/loses the original signup verification email, there is currently no POST /auth/resend-verify-email (or equivalent) to trigger a fresh one; the only way to re-trigger it today is via POST /auth/forgot-password if the account also needs a password reset, or a manual DB/admin action.
  • No public JSON endpoint for shared audit plansGET /public/gtm-audit/{token} only renders HTML; there is no Accept: application/json-aware or separate JSON variant, so nothing (e.g. a future embeddable widget) can consume the shared audit plan as structured data without scraping the HTML.
  • TikTok/Meta OAuth deferred — see Roadmap; only YouTube has a working OAuth publisher today.
  • Custom domains deferred — client portal / branded reports use the app's own domain; there's no support yet for agencies to point a vanity/custom domain at their branded surfaces.
  • Stray/scratch artifacts to remove before git init — the working tree currently contains local run artifacts from development/testing that should not be committed (most are already covered by .gitignore, but confirm they're gone before the first commit rather than relying solely on the ignore rules):
    • backend/smoke_test.db
    • backend/smoke_test.db-journal
    • backend/verify.db
    • backend/verify.db-journal
    • backend/test_write.txt
    • backend/storage/ (local dev asset scratch dir — empty)
    • backend/storage_smoke_test/
    • backend/venv/
    • backend/**/__pycache__/ (several, under app/ and alembic/)
    • frontend/testfile.txt
    • frontend/tsconfig.tsbuildinfo
    • frontend/.next/
    • frontend/node_modules/

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages