Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/growth/marketing-queue-rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Background: [queue package](../../marketing/queue/README.md),
[channel guide](../../marketing/channels/README.md),
[`/week` skill](../../marketing/cowork/week/SKILL.md).

Preview and production use separate Neon resources. Do preview first, every time.
Preview and production use separate Neon resources. Run migrations against preview first, every time. Preview can lag production by several migrations; check its ledger (`threadplane_schema_migrations`) before assuming only the newest one applies.

The marketing tables live in the lifecycle Growth database; there is no separate marketing database. The lifecycle dispatcher reads them through its own `DATABASE_URL`, so `MARKETING_DATABASE_URL_PREVIEW` must equal the preview lifecycle deployment's `DATABASE_URL` and `MARKETING_DATABASE_URL_PRODUCTION` must equal the production lifecycle deployment's `DATABASE_URL`. Do not create a new database.

Expand All @@ -25,7 +25,7 @@ The marketing tables live in the lifecycle Growth database; there is no separate

Store it in your password manager. Lose it and every stored credential must be re-authorized. Use one key for preview and production, or two, but set the same key locally and on the matching lifecycle deployment.
3. A Vercel Blob read-write token (`BLOB_READ_WRITE_TOKEN`). The queue CLI uploads post media with it.
4. A website deploy hook, created in the website's Vercel project settings (Git, Deploy Hooks). The dispatcher calls it when a blog post's `publishAt` arrives so the page appears. Its URL is `MARKETING_WEBSITE_DEPLOY_HOOK_URL`.
4. A website deploy hook on branch `main`, created in the website's Vercel project settings (Git, Deploy Hooks) or with `vercel api /v2/projects/<project id>/deploy-hooks -X POST -f name=marketing-blog-publish -f ref=main`. The dispatcher calls it when a blog post's `publishAt` arrives so the page appears. It builds and promotes `main` the same way every push to `main` already does through the Git integration. Its URL is `MARKETING_WEBSITE_DEPLOY_HOOK_URL`; the production hook is `marketing-blog-publish`.

### Local environment (your machine, `.env`)

Expand Down Expand Up @@ -98,7 +98,9 @@ Afterwards, remove `X_ACCESS_TOKEN` and `X_REFRESH_TOKEN` from your local `.env`

LinkedIn tokens last 60 days and self-serve apps get no refresh token. Put a recurring reminder on your calendar for about 55 days after each authorization, and re-run `npm run marketing:channels:linkedin:auth -- --target=production`. The dispatcher refuses to post to LinkedIn once the token has expired.

## 4. Preview dry run
## 4. Preview dry run (optional, manual tick)

Nothing ticks a preview lifecycle on its own. Vercel runs cron jobs only on production deployments, and the website sets `LIFECYCLE_DAWN_URL` only for production, so a queued preview row waits forever. Preview is still the place to rehearse migrations and the queue CLI. The first rollout (2026-10-31) skipped this section and used the production check in section 5 instead. To exercise the deployed dispatcher without posting, you must trigger the preview tick yourself with the preview `LIFECYCLE_SERVICE_SECRET` after each step below.

1. On the preview lifecycle deployment set `MARKETING_POSTING_ENABLED=true`, `DRY_RUN=1`, and `MARKETING_DRY_RUN_DIR=/tmp/marketing-dry-runs`, then redeploy. With `DRY_RUN=1` the adapters return synthetic results and post nothing. The dry-run adapter writes a payload file, and the default directory (`tmp/marketing/dry-runs` under the working directory) is read-only on Vercel, so without `MARKETING_DRY_RUN_DIR` the write throws and the row is marked `failed` (`post_failed`). Never set `DRY_RUN` on production lifecycle.
2. Queue one X post about five minutes out and approve it:
Expand Down Expand Up @@ -126,8 +128,9 @@ Do the dev.to backlog migration (section 6) before this step, so no backlog item

1. Confirm migration 0011, all lifecycle variables, and all three authorizations are in place for production.
2. Deploy the website before enabling posting: its operator-alert allow-list must know `marketing_dispatch_failed`, or the cron call answers 503 when that alert fires. The merge that ships this branch deploys both; a single merge covers it.
3. Queue one real X post you are happy to publish, `approve` it with `--target=production`, then set `MARKETING_POSTING_ENABLED=true` on the production lifecycle deployment and redeploy.
4. After the due tick, confirm with `list --target=production` that the row is `posted` exactly once and that the post is on X. Do not run `/week` for production until this works.
3. Set `MARKETING_POSTING_ENABLED=true` on the production lifecycle deployment and redeploy the current production deployment (`vercel redeploy <url> --environment production`) so it picks up the variables. Nothing should be due yet.
4. After the next tick, confirm a clean run: the website's production logs show `/api/cron/lifecycle` with no `[lifecycle-operator-alert] marketing_dispatch_failed` line, and no alert email arrives.
5. Queue one real X post you are happy to publish a few minutes out and `approve` it with `--target=production`. After the due tick, confirm with `list --target=production` that the row is `posted` exactly once and that the post is on X. A misconfiguration fails that one row and emails you; failures are never retried, so it cannot double-post. Do not run `/week` for production until this works.

## 6. Migrate the dev.to backlog

Expand Down
Loading