Developer docs
Quality

User flows

How an actor moves through the product: step, screen, endpoint.

9 journeys · 5 complete. A flow is only done when every step is — press play to walk one.

UF-001

Sign up → organization created → dashboard

done

The bootstrap path. Creates the organization and its owner in one transaction, so there is never an org without an owner or a user without a tenant.

5 steps · 5 done
  1. Submit the signup form

    /signupPOST /api/v1/auth/signup

  2. API creates Organization + owner User in one transaction

  3. First audit row written

  4. Tokens stored, session established

  5. Land on the command centre

    /dashboardGET /api/v1/auth/me

Claude: The signup UI is not built yet — the API path is, and is covered by tests.

UF-002

Read the build board

done

What is built, what is next, how it is architected, what is tested, and the docs — without leaving the app or opening the repo.

5 steps · 5 done
  1. Open the landing page

    /

  2. Follow through to the command centre

    /dashboard

  3. Overview shows progress and what's coming up, computed across sections

    /dashboard

  4. Open the Features tab, expand an item for its remarks and updates

    /dashboard

  5. Open the Docs tab and read any doc in place

    /dashboard

Claude: Server-rendered from data/dashboard/*.json and docs/*.md — no client fetch, no waterfall.

UF-003

Plan a quarter for a project

planned

The core planning loop: brief in, plan out, calendar populated.

5 steps · 0 done
  1. Create a project

    /projects/newPOST /api/v1/projects

  2. Write the campaign brief — audience, offer, budget, success metric

    /projects/{id}/briefPOST /api/v1/projects/{id}/brief

  3. Generate a plan (channel mix, budget split, cadence)

    /projects/{id}/planPOST /api/v1/projects/{id}/plan/generate

  4. Edit the plan — the AI output is a draft, not a decision

    /projects/{id}/planPATCH /api/v1/projects/{id}/plan

  5. Accept — plan expands into calendar slots across the three tracks

    /projects/{id}/schedulerPOST /api/v1/projects/{id}/plan/accept

Claude: Step 4 is deliberate: a generated plan that lands straight on the calendar removes the judgement the plan needs.

UF-004

Agency drafts → lead approves → publishes

planned

The approval gate in its normal shape. An outside contributor produces work; an internal owner takes responsibility for it before it ships.

7 steps · 0 done
  1. Agency member logs in, sees only assigned projects

    /projectsGET /api/v1/projects

  2. Drafts a multi-channel post with per-channel overrides

    /composerPOST /api/v1/posts

  3. Submits for approval

    /composerPOST /api/v1/posts/{id}/submit

  4. Lead is notified

  5. Lead reviews and approves (or rejects with a comment)

    /approvalsPOST /api/v1/posts/{id}/approve

  6. Post is scheduled into a calendar slot

    /calendarPOST /api/v1/posts/{id}/schedule

  7. Worker publishes at the slot time; failures retry with backoff

Claude: Step 1 is the security-critical one: org scope is not enough for an agency user, the project assignment is checked too.

UF-005

Agent proposes → human approves → it is applied

done

The agentic loop, and the gate that makes it safe. An agent writes its whole proposal while the run is `awaiting_approval` — nothing has touched content, a budget or a platform at that point, so a rejected run costs model tokens and nothing else. There is deliberately no `running → applied` edge in the state machine.

6 steps · 6 done
  1. Press Run now on an agent

    /projects/:id/agentsPOST /api/v1/agents/runs

  2. Plan + project visibility checked; run written as `queued` and published to RabbitMQ

  3. Worker picks it up (`running`), the agent reads the project, and writes its proposal (`awaiting_approval`)

  4. Proposal appears beside the team it came from

    /projects/:id/agents/proposalsGET /api/v1/agents/runs

  5. Someone with `agents:approve` reads what it found and decides

    /projects/:id/agents/proposalsPOST /api/v1/agents/runs/:id/approve

  6. `apply.ts` runs — the Drafter creates posts, the others record or unlock the next step

    /projects/:id/content

Claude: No auto-apply mode. adwords-adsense planned autopilot as the default; that is where the money risk lives.

Claude: Marked done. The flow shipped; this row still described the planned shape, including a /approvals screen that now means something else — post sign-off, not agent proposals. Full walkthrough with diagrams in docs/AGENT_FLOW.md.

UF-006

Answer 'what is running and what is it returning?'

planned

The success metric from the brief: under 60 seconds, no spreadsheet.

4 steps · 0 done
  1. Open the KPI surface

    /kpisGET /api/v1/analytics/kpis

  2. Every channel, organic and paid, with spend, return and freshness

    /kpis

  3. Compare against vertical benchmarks

    /kpisGET /api/v1/ads/benchmarks

  4. Drill into a project or a channel

    /kpis/{scope}

Claude: Step 2 carries an as-of timestamp per source. A stale number that looks live is worse than no number.

UF-007

Connect a channel

planned

OAuth into a platform and keep the connection alive without anyone watching it.

5 steps · 0 done
  1. Pick a platform

    /settings/channels

  2. Redirect to the platform's consent screen

    GET /api/v1/channels/{platform}/oauth/start

  3. Callback exchanges the code; tokens encrypted at rest

    GET /api/v1/channels/{platform}/oauth/callback

  4. Profile fetched, account listed as connected

    /settings/channels

  5. Hourly job refreshes tokens expiring within 24h; a 6-hourly health check flags a break before a publish fails

Claude: Step 5 matters more than it looks — a silently expired token surfaces as a mysterious publish failure days later.

UF-008

Ship a release: merge → release → nonprod → prod

done

How code reaches customers. Merging deploys nothing: every merge builds signed api + web images already stamped with the next version; a release names one of those builds vX.Y.Z (nothing is rebuilt); the promoter puts it on nonprod; a person on PROD_DEPLOYERS deploys the same images to prod through gates. The next patch number is set automatically after each release (D-066), so only a minor or major release needs a version PR. Runbook: deploy/README.md → Shipping a release.

8 steps · 8 done
  1. Merge a PR with its NEXT/ note (impact: patch, minor or major). CI builds and signs api + web images stamped <nextStableVersion>-rc.N

    GitHub → Pull request.github/workflows/container-candidate.yml

  2. Minor or major release only: a one-line PR sets nextStableVersion first (e.g. 0.12.0), and the release is cut after it merges. A patch needs nothing: the number is already set

    container-candidate.json (e.g. PR #431)

  3. Cut the release from the newest main candidate: version = nextStableVersion, candidate-manifest = <artifact id>@<digest>. It writes CHANGELOG/vX.Y.Z.md from NEXT/, the tag and the GitHub Release

    Actions → container release.github/workflows/container-release.yml

  4. Automatic: next version moves nextStableVersion one patch on (0.11.1 → 0.11.2) and pushes it to main with the release App

    Actions → next version.github/workflows/next-version.yml

  5. Automatic: the promoter deploys the release to nonprod; any new migration runs there first

    Actions → promote → deploy nonprodpromote.yml → deploy-nonprod.yml

  6. Check the change on nonprod; it reports the release. The promoter's summary says ready for prod after a 24 h soak, but deploy prod only requires that nonprod ran it

    https://nonprod.omnichan.studioGET /api/v1/version

  7. Deploy prod: signatures, ran-on-nonprod, downgrade and migration gates (tick migrations_acknowledged when the release adds one), database backup to R2, migrate, roll with a health check

    Actions → deploy prod (release = vX.Y.Z).github/workflows/deploy-prod.yml

  8. Confirm prod serves the release. Roll back = deploy the previous release with allow_downgrade: images only, never the database

    https://studio.lightningleap.storeGET /api/v1/version

Claude: First run end to end: v0.10.0 reached nonprod and then prod through these steps (deploy prod run 37619410085, every gate green).

Claude: Commands. Candidate: run=$(gh run list --workflow container-candidate.yml --branch main --event push --status success --limit 1 --json databaseId --jq '.[0].databaseId'); gh api repos/verJSON/marketing-studio/actions/runs/$run/artifacts --jq '.artifacts[] | select(.name | startswith("container-candidate-")) | "\(.id)@\(.digest)"'. Release: gh workflow run container-release.yml --ref main -f version=X.Y.Z -f candidate-manifest=<id>@<digest>. Prod: gh workflow run deploy-prod.yml --ref main -f release=vX.Y.Z (add -f migrations_acknowledged=true when it adds a migration).

Claude: v0.11.1 went through every step: release run 37775655720; next version set 0.11.2 on its first run (b0a37a0f); nonprod 37775951946; prod 37776609135 with the post_origin migration acknowledged.

UF-009

Ship a database migration: expand now, contract later

done

How a schema change reaches prod safely. Every deploy runs pending migrations automatically, before the new api and worker start; nonprod runs them first; prod refuses until a person acknowledges them, and backs the database up before migrating. There are no down-migrations: a rollback swaps images, never the database, so every migration must be safe for the previous release to run against. Add (table, defaulted or nullable column, index) now; rename or drop in a later release.

7 steps · 7 done
  1. Change the schema and generate the migration; add any new table to TABLES in the test setup so each test starts clean

    apps/api/prisma/schema.prisma, apps/api/src/test/setup.tsnpm run db:migrate (prisma migrate dev)

  2. Keep it additive. A rename or drop waits for a later release, once no running code reads the old shape

    PR review

  3. CI applies every migration to a fresh test database and checks each file is pure SQL

    ci / build-testapps/api/src/test/migrations-clean.test.ts

  4. Nonprod: a one-shot migrate container runs prisma migrate deploy before api and worker start; a failure stops the roll and the old version keeps serving

    Actions → deploy nonproddocker-compose.prod.yml → migrate, deploy/roll-vm.sh

  5. Prod: deploy prod stops at the migration gate and lists the new migrations; run it again with migrations_acknowledged=true once they are read

    Actions → deploy proddeploy-prod.yml → Downgrade and migration gates

  6. Backup to R2, then migrate, then roll with a health check. A failed migration aborts the roll and the previous release keeps serving (D-047)

    Actions → deploy prod → backup, deploydeploy/db-backup.sh, deploy/roll-vm.sh

  7. The promoter never deploys a release that adds a migration to prod by itself: a person always does

    Actions → promote → run summaryscripts/promote-plan.mjs

Claude: v0.11.1 shipped 20261007120000_post_origin this way: an enum and a defaulted column, nothing back-filled. Next: #428 adds the seo_research_results table (FK to projects, on delete cascade) in v0.12.0.