# Marketing Suite — architecture design (Descrybe v2) **Status:** Design only — no implementation in this doc. **Date:** 2026-08-04 **Repos:** `f:/laragon/www/_MY/descrybe-v2` **Inputs:** [PRICING-AND-USER-GROWTH.md](../../descrybe/PRICING-AND-USER-GROWTH.md) (sibling), [process-and-sell-summary.md](process-and-sell-summary.md), [features.md](features.md), existing `internal/billing`, `internal/woocommerce`, `internal/processing`, `internal/mail`, Settings + Nav. --- ## 0. North star Descrybe’s moat remains **feeds → map → process → export/Woo**. Marketing add-ons sit **on top of a live catalog + Woo connection**, not as a second product that competes with “paste ChatGPT into a plugin.” ``` [ Catalog + Brand kit ] │ ├── Product AI copy (existing pipeline) ── credits / BYOK │ └── Marketing Suite ├── Audience (orders + opt-in) ├── Campaigns (seasonal / category / product) ├── Reviews (sync + social proof in copy) └── SEO recommendations (catalog-derived) ``` **Principle:** Defaults get a merchant from “Black Friday email for buyers of Category X” in minutes. Advanced (custom prompts, BYOK, segment builders) stays behind optional panels. --- ## 1. Context from today’s codebase | Area | Today (v2) | Implication for Marketing | |------|------------|---------------------------| | **Woo** | Company-level `woocommerce_configs`; **outbound product push** only (`internal/woocommerce`); worker claims sync jobs | Must **add inbound** order (+ later review) sync; keep credentials encrypted; reuse same store config | | **Credits** | `credit_balances` + `AssertCanStartProcessing` / `ConsumeCredits(..., featureName)` | Extend with feature names: `campaign_copy`, `seo_recommend`, `brand_kit_ai`; same wallet | | **Plans** | `plans` / `company_plans` + SKU caps; public Free/Starter/Growth/Business/Enterprise in `pricing-data.ts` | Need **feature flags per plan** (not only meters) for Marketing | | **AI** | Worker `Completer` (OpenAI or heuristic); no tenant BYOK yet | Campaign/SEO/Brand AI share Completer + future BYOK seam | | **Mail** | Platform SMTP for invites (`internal/mail`); noop if disabled | **Campaign send ≠ platform invite SMTP** — tenant-verified provider required | | **Nav / Settings** | Nav: PIM core + WooCommerce extra; Settings tabs: profile / company / api-keys / team | Add **Marketing** nav group; Settings → **Integrations** tab (Woo, Email, Reviews, AI keys) | | **Jobs** | DB claim + `NOTIFY` for processing + Woo | Same pattern for `order_sync`, `review_sync`, `campaign_send`, `campaign_generate` | ### Pricing doc conflict (resolve explicitly) [PRICING-AND-USER-GROWTH.md](../../descrybe/PRICING-AND-USER-GROWTH.md) currently proposes **Free = 50 AI credits / mo**. **This suite design requires Free = ZERO AI credits** and gates campaigns / SEO AI / brand AI. **ASSUMPTION (recommended):** Align product policy as: | Plan | Core product AI | Marketing AI (campaigns / SEO / brand kit AI) | |------|-----------------|-----------------------------------------------| | **Free** | **0 credits** (map/export/Woo test only; reverse-trial exception below) | **Blocked** | | **Starter+** | Included pack per plan | Included; burns same credit wallet (or BYOK) | | **Reverse trial (14d Growth)** | Full Growth credits | Full Marketing allowed | Update `pricing-data.ts` + plan seeds when implementing — Free must not advertise “50 AI credits” if policy is zero. --- ## 2. Product modules ### 2.1 Email campaigns **Jobs:** Draft → preview → schedule/send seasonal or catalog-driven emails using Descrybe catalog + Woo audiences. #### Campaign types (MVP → later) | Type | Trigger / audience seed | Copy source | |------|-------------------------|-------------| | **Seasonal template** | Christmas, Black Friday, New Year, Summer (system templates) | Default prompts + brand kit | | **By category** | Products in selected category tree node(s) | Catalog titles/images + formula fields | | **By product set** | Explicit SKUs / product IDs / collection | Same | | **Purchase-based** | Bought X / not bought Y (needs order sync) | Same + segment filters | #### Prompt model (mirror product formulas) ``` campaign.prompt_mode = "default" | "custom" campaign.system_prompt_override? // only if custom + plan allows campaign.user_prompt_vars = { season, category_names, product_snippets, brand_voice, cta_url } ``` - **Default:** curated seasonal/category prompts + Brand kit (voice, tone, banned words). - **Custom:** editable system/user prompt (Growth+); stored per campaign, versioned. - **Preview:** generate N sample emails (1–3) before send; each preview burns credits (or BYOK). #### AI path | Path | When | Billing | |------|------|---------| | **Managed** | Default | Debit `campaign_copy` (+ token packs via existing `openai_token_k`) | | **BYOK** | Growth add-on / Business+ | Completer uses tenant key; **no credit burn for inference**; still need paid plan feature flag | **Never send AI-generated body without human confirm** on first send of a campaign (checkbox “I reviewed the preview”). Resends of an approved version can skip. #### Data model (sketch) ``` marketing_campaigns id, company_id, name, type, status (draft|scheduled|sending|sent|cancelled) season_key?, category_ids[], product_ids[] audience_query jsonb -- segment DSL (see §3) prompt_mode, prompts jsonb subject, preview_text, body_html, body_text schedule_at?, sent_at? ai_provider (managed|byok), credit_cost_est created_by, created_at, updated_at marketing_campaign_sends id, campaign_id, recipient_hash, email_encrypted_or_token status (queued|sent|bounced|unsubscribed|failed) provider_message_id?, error?, sent_at? marketing_campaign_events -- opens/clicks later (P2) ``` Worker jobs: `campaign_generate`, `campaign_send_batch` (chunked, rate-limited). --- ### 2.2 Order sync (Woo → audience) **Goal:** Build segments like “purchased product/category X in last 90d AND never purchased Y.” #### Sync design - Extend Woo **Integrations** config (same `woocommerce_configs` row or sibling `integration_woocommerce_orders`): - `orders_enabled`, `orders_last_synced_at`, `orders_cursor` (modified_after / page) - Worker job `woo_order_sync` (incremental, bounded pages per tick) - REST: `GET /wp-json/wc/v3/orders` (+ line items); map to local tables ``` woo_customers company_id, woo_customer_id, email_hash, email_enc, name, marketing_opt_in, unsubscribed_at, ... woo_orders company_id, woo_order_id, customer_id, status, ordered_at, currency, total, raw_meta jsonb woo_order_items company_id, order_id, sku, product_id_local?, woo_product_id, quantity, category_ids_snapshot[] ``` **Privacy:** store emails encrypted at rest; display masked; use `email_hash` for uniqueness. Prefer Woo `marketing_opt_in` / billing consent fields when present. #### Audience DSL (JSON, server-validated) ```json { "op": "and", "rules": [ { "type": "purchased_sku", "skus": ["ABC-1"], "since_days": 365 }, { "type": "not_purchased_sku", "skus": ["ABC-2"], "since_days": 365 }, { "type": "purchased_category", "category_ids": ["uuid…"], "since_days": 90 }, { "type": "marketing_opt_in", "equals": true } ] } ``` Resolver compiles to SQL with **hard filters**: `unsubscribed_at IS NULL`, `marketing_opt_in = true`, bounce suppression list. Always AND these safety filters server-side (client cannot disable). **ASSUMPTION:** Guests without account email are skipped unless order billing email present and opted-in. --- ### 2.3 Reviews sync | Provider | Phase | Notes | |----------|-------|-------| | **WooCommerce product reviews** (`/wp-json/wc/v3/products/reviews`) | **P1** | Same store credentials; incremental sync | | **Placeholder providers** (Judge.me, Yotpo, Trustpilot) | **P2** | Interface only: `ReviewProvider` with `ListSince(cursor)`; UI “Coming soon” + webhook stub | ``` product_reviews company_id, provider, external_id, product_sku, rating, title, body, author_hash reviewed_at, synced_at, usable_in_marketing bool ``` Uses: social proof blocks in campaign templates; optional “mention top reviews” in default prompts; SEO module “products with < N reviews.” --- ### 2.4 SEO recommendations (from catalog) **Not** a full site crawler in P0. Catalog-first checks that reuse processed products + categories + standard fields: | Check | Signal | Output | |-------|--------|--------| | Thin title / description | Length, uniqueness | Rewrite suggestion (AI = paid) | | Missing attributes | Required attrs empty | Fill via process job CTA | | Duplicate titles | Collision count | Merge / differentiate | | Weak category paths | Depth / orphan | Taxonomy tip | | Image gaps | No primary image | Upload / feed map tip | | Keyword coverage | Brand kit primary keywords vs title | Nudge | ``` seo_recommendation_runs id, company_id, status, created_at, summary jsonb seo_recommendations id, run_id, product_id?, severity, code, message, suggested_action ai_draft?, applied_at? ``` - **Free:** show **rule-based** list only (no AI drafts). - **Paid:** “Generate rewrite” → `seo_recommend` credits / BYOK → optional apply to product fields via existing product PATCH. --- ### 2.5 Brand kit Company-scoped creative constraints for **product copy + campaigns**. ``` brand_kits company_id PK voice_summary text -- short “we sound like…” tone_tags text[] -- e.g. friendly, expert, playful banned_phrases text[] preferred_phrases text[] primary_color, secondary_color logo_url / logo_file_id fonts jsonb? -- optional locale_default updated_at ``` - **Manual edit:** available Starter+ (or Free read-only empty kit?). **ASSUMPTION:** Free can **view** empty kit + upgrade CTA; edit + AI-assist = paid. - **AI assist (“Infer brand from catalog”):** paid; feature `brand_kit_ai`. - Injected into processing enhance prompts **and** campaign defaults (single source of truth). UI: Settings → Company **or** Marketing → Brand kit (prefer **Marketing → Brand kit** with deep-link from Integrations). --- ## 3. Plan gating & Free tier ### 3.1 Feature matrix | Capability | Free | Starter | Growth | Business+ | |------------|------|---------|--------|-----------| | SKU / feed / storage meters | Per pricing ladder | ✓ | ✓ | ✓ | | **AI credits** | **0** | Pack | Pack | Pack / BYOK | | Product processing AI | ✗ (unless reverse trial) | ✓ | ✓ | ✓ | | Marketing nav (read-only teasers) | Teaser + upgrade | ✓ | ✓ | ✓ | | Campaigns create/send | ✗ | ✓ (caps) | ✓ | ✓ | | Campaign AI generate | ✗ | Credits | Credits/BYOK | ✓ | | Order sync | ✗ (or read-only test 1 page?) | ✓ | ✓ | ✓ | | Audience “bought X not Y” | ✗ | ✓ | ✓ | ✓ | | Reviews sync (Woo) | ✗ | ✓ | ✓ | ✓ | | SEO rule-based | ✓ (limited rows) | ✓ | ✓ | ✓ | | SEO AI rewrite | ✗ | ✓ | ✓ | ✓ | | Brand kit edit | ✗ | ✓ | ✓ | ✓ | | Brand kit AI | ✗ | ✓ | ✓ | ✓ | | BYOK | ✗ | ✗ | Add-on | Included | **ASSUMPTION:** Starter is the smallest plan that unlocks Marketing Suite send + order sync. Free stays acquisition for **feed aha**, not email ESP. ### 3.2 Enforcement seams (reuse billing) Extend `internal/billing`: ```go // Pseudocode — design only AssertFeature(ctx, companyID, "marketing.campaigns") AssertFeature(ctx, companyID, "marketing.order_sync") AssertCanSpendCredits(ctx, companyID, feature, est) ``` - Plan row gains `features jsonb` (or `plan_entitlements` table). - API returns `403` + machine code `feature_gated` / `insufficient_credits` for UI `UpgradeBanner`. - Reverse trial: temporary Growth entitlements; day 15 → Free (0 credits, Marketing hard-off). Persist draft campaigns read-only. ### 3.3 Caps (Starter starter-limits) | Meter | Starter (example) | Growth | |-------|-------------------|--------| | Campaigns / mo | 5 | 50 | | Recipients / send | 2_000 | 25_000 | | Order sync lookback | 180d | 730d | | SEO AI rewrites / mo | 50 | 1_000 | Exact numbers tunable; enforce server-side. --- ## 4. Integration style (UX IA) ### 4.1 Settings → Integrations Replace scattered `/woocommerce` as the *only* integration home. Keep `/woocommerce` as redirect → Settings Integrations for one release if needed. **Settings tabs:** `profile` | `company` | `team` | `api-keys` | **`integrations`** Integrations cards: 1. **WooCommerce** — store URL, keys, test connection, product sync toggle, **order sync** toggle, **reviews sync** toggle 2. **Email sending** — provider (Resend / SES / SMTP / Postmark), from-domain, **verification status**, rate limits 3. **AI keys (BYOK)** — Growth+; encrypted; never logged 4. **Reviews providers** — Woo (active) + placeholders ### 4.2 Marketing nav New top-level group (after Export Feeds / before Billing): ``` Marketing ├─ Campaigns ├─ Audiences ├─ Reviews ├─ SEO └─ Brand kit ``` Free users see routes with locked empty-states + upgrade CTAs (not 404). --- ## 5. Safety (non-negotiable) | Rule | Implementation | |------|----------------| | **No send without verified provider** | `email_provider.status == verified` AND DNS/domain checks passed; else Send button disabled + API `412 email_not_verified` | | **Opt-out / unsubscribe** | Every email: `List-Unsubscribe` + one-click POST; landing page; set `unsubscribed_at`; suppress forever until re-opt-in via Woo/consent | | **Opt-in required** | Audience resolver always requires marketing consent | | **Rate limits** | Per-company send RPM/RPH; provider caps; exponential backoff; circuit breaker on bounce rate > threshold | | **PII** | No emails in logs; hash + encrypt; admin support tools mask by default | | **Double opt-in for imported lists** | If CSV import ever added (P2): require confirmation — Woo-sourced opted-in only in P0/P1 | | **Abuse** | Cap Free teasers; paid caps; platform admin kill-switch per company | | **Legal copy** | Footer template mandatory (company address, unsubscribe) before schedule | Platform invite SMTP (`internal/mail`) **must not** be used for marketing blasts. --- ## 6. UX principles 1. **Happy path wizard:** Pick template (BF/Christmas) → pick category/products → auto audience suggestion → generate 1 preview → connect email if missing → schedule. 2. **Advanced accordion:** custom prompts, raw audience DSL, UTM params, send-time optimization (P2). 3. **Reuse catalog chrome:** product pickers, category tree, UpgradeBanner, processing job progress patterns. 4. **Empty states teach the core path:** “Connect Woo + sync orders to target buyers of X.” 5. **Credit honesty:** show estimated credit cost before Generate; BYOK badge when active. --- ## 7. Interactive tutorial (activation) Extend the pricing doc’s ≤15 min aha into a **guided checklist** (dashboard + optional spotlight tour): | Step | Route / action | Done when | |------|----------------|-----------| | 1. Add feed | `/feeds` | Feed created with URL/file | | 2. Map fields | `/feeds/{id}/mapping` | Required fields mapped (auto-map OK) | | 3. Process sample | `/processing` | ≥10 products `completed` (credits: trial/paid) | | 4. Export or Woo push | `/export-feeds` or Integrations | Export generated **or** Woo test sync OK | | 5. Brand kit (paid) | `/marketing/brand-kit` | Voice saved **or** skipped with “later” | | 6. Campaigns (paid) | `/marketing/campaigns` | Draft campaign created (send optional) | - Persist `onboarding_progress` on `company_settings`. - Free users complete 1–4; 5–6 show locked steps with value props. - Tutorial deep-links must survive refresh; dismissible; “Replay tour” in help. Aligns with [process-and-sell-summary.md](process-and-sell-summary.md) happy path + Marketing as step after export. --- ## 8. Technical architecture (v2 packages) ``` apps/api/internal/ marketing/ # campaigns, audiences, brand kit, seo services integrations/ email/ # provider interface: Send, VerifyDomain, Webhooks reviews/ # ReviewProvider billing/ # + AssertFeature, feature costs woocommerce/ # + OrdersClient, ReviewsClient, sync workers apps/api/sql/schema/ 01x_marketing.sql # tables above 01y_integrations_email.sql apps/web/src/routes/ marketing/campaigns/ marketing/audiences/ marketing/reviews/ marketing/seo/ marketing/brand-kit/ settings/ # + integrations tab unsubscribe/[token]/ # public ``` **Completer sharing:** marketing generation calls the same `processing.Completer` interface; inject company BYOK client when entitlement + key present. **Idempotency:** send batches keyed by `(campaign_id, recipient_hash)`; sync cursors monotonic. --- ## 9. API sketch (dashboard + future public) | Method | Path | Notes | |--------|------|-------| | GET/PATCH | `/api/integrations/woocommerce` | Existing + order/review flags | | POST | `/api/integrations/woocommerce/sync-orders` | Enqueue | | GET/PATCH | `/api/integrations/email` | Provider config | | POST | `/api/integrations/email/verify` | Start verification | | GET/PATCH | `/api/marketing/brand-kit` | | | GET/POST | `/api/marketing/campaigns` | | | POST | `/api/marketing/campaigns/{id}/generate` | Credits gate | | POST | `/api/marketing/campaigns/{id}/schedule` | Requires verified email | | POST | `/api/marketing/audiences/preview` | Count + sample (masked) | | POST | `/api/marketing/seo/runs` | | | POST | `/api/public/unsubscribe` | Token auth | Public API v1 exposure of Marketing = **Business+ / later**; dashboard session auth first. --- ## 10. Risks & open decisions | Topic | Risk | Recommendation | |-------|------|----------------| | Free AI credits vs pricing doc | Conflicting promises | **Ship Free = 0 AI**; update pricing doc + `pricing-data.ts` | | Becoming an ESP | Deliverability / spam liability | Verified domain only; start with low caps; consider “export to Klaviyo” P2 | | Order PII | GDPR | Encrypt, retention policy, delete-with-company, DPA | | Woo permission scopes | Keys may lack orders/reviews | TestConnection checks endpoints; clear UI errors | | Custom prompts abuse | Prompt injection / brand damage | Length limits; strip tools; require preview ack | **Open questions (non-blocking for P0 scaffolding):** 1. First ESP: Resend vs SES vs tenant SMTP only? 2. Starter recipient cap exact number? 3. Free SEO rule-based: show all issues or cap at 20? --- ## 11. Sibling-agent checklist ### P0 — foundation (do first; unblocks everything) - [ ] **P0.1** Align Free plan: **0 AI credits**; Marketing feature flags on `plans`; update `pricing-data.ts` + seeds; reverse-trial keeps Growth marketing. - [ ] **P0.2** `billing.AssertFeature` + credit feature names (`campaign_copy`, `seo_recommend`, `brand_kit_ai`); wire `UpgradeBanner` codes. - [ ] **P0.3** Settings → **Integrations** tab; move Woo config UX there (redirect old `/woocommerce`). - [ ] **P0.4** Marketing nav shell + locked empty states for Free. - [ ] **P0.5** Brand kit schema + CRUD (manual fields only). - [ ] **P0.6** Email provider integration model + **verified-only send gate** (even if provider is stubbed behind interface). - [ ] **P0.7** Campaign draft CRUD + default seasonal templates + generate-preview job (no mass send yet). - [ ] **P0.8** Unsubscribe token endpoint + suppression list. - [ ] **P0.9** Woo **order sync** (incremental) + `woo_orders` / items / customers tables. - [ ] **P0.10** Audience preview: purchased X / not purchased Y + opt-in hard filters. - [ ] **P0.11** Onboarding tutorial steps 1–4 wired to real completion signals; 5–6 gated. - [ ] **P0.12** Safety tests: cannot send if unverified; cannot include unsubscribed; Free cannot generate campaign AI. ### P1 — sellable Marketing MVP - [ ] **P1.1** Campaign schedule/send batches via verified provider + rate limits + bounce basic handling. - [ ] **P1.2** Custom vs default prompts UI; credit estimate + BYOK seam (if BYOK platform work ready). - [ ] **P1.3** Woo product **reviews** sync + Reviews Marketing page. - [ ] **P1.4** SEO recommendation runs (rule-based + paid AI rewrite). - [ ] **P1.5** Brand kit injection into product enhance + campaign defaults. - [ ] **P1.6** Cap enforcement (campaigns/mo, recipients/send). - [ ] **P1.7** Tutorial steps 5–6 + “first Black Friday campaign” recipe. - [ ] **P1.8** Ops: kill-switch, send metrics on billing overview. ### P2 — expand - [ ] **P2.1** Placeholder review providers (Judge.me / Yotpo) + webhooks. - [ ] **P2.2** Open/click tracking; A/B subjects. - [ ] **P2.3** Export audience / campaign to Klaviyo/Mailchimp. - [ ] **P2.4** Site crawl SEO (beyond catalog). - [ ] **P2.5** Public API for campaigns. - [ ] **P2.6** Multi-language campaign variants from catalog locales. - [ ] **P2.7** AI “infer brand kit from best products.” --- ## 12. Verification expectations (when built) - Unit: audience SQL compiler, feature gate matrix, unsubscribe token. - Integration: Woo orders pagination mocked; send blocked without verify. - UI: Free user sees locks; Starter can preview generate with credits. - No claim-done without `AssertFeature` coverage on every mutating Marketing route. --- ## 13. Rollback - Feature flags `MARKETING_ENABLED` (platform) + per-plan entitlements. - Schema additive only; drop via goose Down if needed. - Disabling flag hides nav and 404s APIs without deleting drafts. --- ## Related docs - Sibling pricing: `f:/laragon/www/_MY/descrybe/PRICING-AND-USER-GROWTH.md` - [process-and-sell-summary.md](process-and-sell-summary.md) - [features.md](features.md) · [status-and-gaps.md](status-and-gaps.md) · [security-notes.md](security-notes.md)