Skip to content

feat(connectors/distribution): Postiz organic + Meta/TikTok DRAFT + Klaviyo Flows#22

Open
ninan-versioning wants to merge 1 commit into
mainfrom
jared-shoploop/distribution-adapters
Open

feat(connectors/distribution): Postiz organic + Meta/TikTok DRAFT + Klaviyo Flows#22
ninan-versioning wants to merge 1 commit into
mainfrom
jared-shoploop/distribution-adapters

Conversation

@ninan-versioning

Copy link
Copy Markdown
Owner

Summary

New package @shoploop/connector-distribution — the distribution layer that pushes Creative Packs (open-imagine creative_pack.v1) to channels. Four adapters behind one shared AdapterInterface; MCP, Temporal, and the CLI are thin shells over a single dispatch core. Mirrors the shape of the Shopify (PR #15) and Google Ads (PR #18) connectors.

CONTRACT: consumes open-imagine/contracts/creative_pack.v1.json (referenced via // CONTRACT:, not vendored).

Adapters

Channel Mode Creates Status invariant
Postiz organic one schedule per network (meta_page, tiktok_business, ig_business, x) n/a (organic only)
Meta paid_draft Campaign → Ad Set → Ad, all status=PAUSED status=ACTIVELivePublishForbidden
TikTok paid_draft Campaign → Ad Group → Ad, all operation_status=DISABLE operation_status≠DISABLELivePublishForbidden
Klaviyo transactional shoploop.creative_pack.ready Server Event (Flow owns send timing) n/a (event only; 409 = success)

DRAFT-only — enforced twice, no live path

Paid adapters are DRAFT-only in this PR. There is no live-publish code path, not even behind a flag. Two LivePublishForbidden guards scan every outbound payload before it leaves the process:

  • meta.tsassertNoLivePublish() throws if any status / effective_status / configured_status === "ACTIVE".
  • tiktok.tsassertNoLivePublish() throws if any operation_status !== "DISABLE".

adapters.test.ts asserts both guards throw on a mutated (ACTIVE / ENABLE) payload, and that every real outbound Meta/TikTok body is PAUSED/DISABLE.

Idempotency

Keyed by (pack_id, channel, audience_id) at two layers: a LibSQL dispatch_log table (non-failed row short-circuits, failed row retries) and the Temporal workflow id dispatch:<channel>:<packId>:<audienceHash> with REJECT_DUPLICATE. FanoutPackWorkflow fans out to all channels and returns partial success — one channel failing does not fail the workflow.

Varitea acceptance

shoploop-distribution dispatch --pack test/fixtures/varitea_pack.json --channels all produces 1 Klaviyo event + 1 Postiz organic schedule + 1 PAUSED Meta ad + 1 DISABLE TikTok ad, with channel URLs (verified locally against a mock standing in for all four APIs):

postiz   sent      meta_page:pz_…,tiktok_business:pz_…,ig_business:pz_…,x:pz_…  https://postiz.local/p/x
meta     draft     mad_1   https://business.facebook.com/adsmanager/manage/ads?act=…&selected_ad_ids=mad_1
tiktok   draft     ttad_1  https://ads.tiktok.com/i18n/perf/creative?aadvid=…
klaviyo  sent      pack_varitea_oolong_hero_001:klaviyo:high-mountain-oolong:default

Cost

Postiz $0–$20/mo (self-host or Cloud) · Meta/TikTok $0 (DRAFT-only ⇒ no ad spend) · Klaviyo events included · LibSQL $0 · co-located on the existing t4g.small. Total: $0–$20/mo.

Test plan

  • npm run typecheck — clean
  • npm run build — clean
  • npm test — 19/19 pass (adapters, workflows, mcp)
  • adapters.test.ts — Meta/TikTok DRAFT invariants, both LivePublishForbidden guards, Klaviyo 409→success, Postiz multi-network aggregation, BrandVoiceViolation
  • workflows.test.ts — FanoutPack across 4 channels, idempotency on retry, partial success when one channel fails
  • mcp.test.ts — in-process SSE; all 5 tools (dispatch_pack, get_dispatch, list_recent_dispatches, cancel_dispatch, list_channels)
  • CLI acceptance run against a local mock (output above)

🤖 Generated with Claude Code

…laviyo Flows

New @shoploop/connector-distribution package: four channel adapters behind a shared
AdapterInterface, with MCP, Temporal, and CLI as thin shells over one dispatch core.

- Postiz (organic): fan-out across meta_page/tiktok_business/ig_business/x
- Meta (paid_draft): Marketing API v20.0, PAUSED campaign/adset/ad
- TikTok (paid_draft): Marketing API v1.3, DISABLE campaign/adgroup/ad
- Klaviyo (transactional): shoploop.creative_pack.ready Flow event; 409 = success

DRAFT-only is enforced twice: payloads are built PAUSED/DISABLE, and a runtime
assertNoLivePublish guard throws LivePublishForbidden on any ACTIVE/non-DISABLE
status before the request leaves the process. No live-publish path exists in this PR.

Idempotency keyed by (pack_id, channel, audience_id) via a LibSQL dispatch_log and
the Temporal workflow id. FanoutPackWorkflow returns partial success.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant