Voyage Roadmap » Events Studio » Journey Builder

Phase plan

Seven phases from the engineering spec. Build order leads with the type foundation, then the gated surface, then access, then the editor/persist/render, then a live end-to-end acceptance walk.

PhaseStatusScope
Phase 1 Verified on branch Type foundation: @voyage/journey-runtime (the JourneyConfig model + 7-stage catalogue + default-order-by-type + pure helpers) and the @voyage/journey-builder package skeleton. No UI/DOM deps in the runtime so the public bundle stays lean.
Phase 2 Verified on branch Shell route + nav skeleton: exported JourneyBuilderRoute, inline gated /journeys + /journeys/:id/edit, a gated primary-nav "Journeys" item (disabled → redirect to dashboard), an honest empty-state list + type-picker modal, and the three-zone editor scaffold rendering a real journey-runtime preview. Gate verified enabled+disabled; axe-clean; no horizontal scroll desktop/mobile. 72c63832.
Phase 0 In flight Access foundation (both layers, both tiers): control-plane feature_catalog flag journey_builder + a dedicated journey.build capability, enforced at app route-guard AND API; SAML JIT configurable attribute mapping; conflict UX = hide nav + redirect-with-toast, never error. Meets the identity-stack security bar. (Deferred through Phases 1–5 by design; now being wired by composing into the existing identity infra.)
Phase 3 Verified on branch Builder surface: the three-zone editor (top bar · ~372px Customize sidebar · live preview canvas with clickable step strip), per-stage settings panels, reorder/enable-disable, custom text + branding/footer, and the live two-way binding that is the WYSIWYG loop. ?step= URL state + per-page document.title. 1cb82688.
Phase 4 Verified on branch Persistence + publish: JourneySurfaceRow + journey_surfaces D1 table (partial unique index = one live per (tenant,journey)), seven v1/journeys endpoints on the events worker, atomic single-live publish via batch([demote, promote]), parameterized + signed-context tenant scoping, admin-gated, audited. Editor Save/Publish wired real (honest offline fallback). SDK-friendly camelCase DTOs + paginated list. 06e290b9.
Phase 5 In flight Public render: PublicJourneyRoute at one slug per journey — sequential branded full-page steps (header · step body · summary rail · Back/Continue), SPA transitions with ?step=, per-journey BrandKit, and input capture via the existing submitPublicRegistration. Preview render and public render share journey-runtime — byte-identical.
Phase 6 Planned End-to-end acceptance on a live deploy: create → configure stages → save draft → publish → load the public journey → complete it → read the captured registration back. Axe-clean, no horizontal scroll at any breakpoint, full keyboard operability, URL-state safe, both access layers enforced.

Turn-by-turn progress live

One row per Codex turn; each independently verified by Claude before acceptance. Chronological, newest at the bottom. All commits are local on feat/journey-builder (not pushed).

TurnStatusScopeCommit
Turn 1 · Phase 1 Verified on branch Built:
  • @voyage/journey-runtime — the JourneyConfig type model + 7-stage STEP_CATALOGUE (core = service/datetime/details/confirmation; tickets event-only) + DEFAULT_ORDER_BY_TYPE + helpers (defaultJourneyConfig, isCoreStep, isStepValidForType, normalizeOrder). Pure types/functions, no UI deps.
  • @voyage/journey-builder — editor package skeleton depending on runtime + jrni-ui/jrni-tokens.
Claude-verified (own runs):
  • build + typecheck both packages clean; journey-runtime tests 7/7 (default-order-per-type, confirmation-last, core-non-removable, tickets-event-only).
  • JourneyConfig + catalogue match engineering spec §2/§3/§4 exactly.
fed0b73d
Turn 2 · Phase 2 Verified on branch Built:
  • JourneyBuilderRoute exported from events-studio-shell; inline gated /journeys + /journeys/:id/edit; gated primary-nav "Journeys" RailItem; journeyBuilder.enabled prop from App.tsx (disabled deep-link → redirect to dashboard).
  • Honest empty-state list (Live/Drafts groups + type filters) + type-picker modal (lands in the editor with a real defaultJourneyConfig); three-zone editor scaffold with a real journey-runtime preview. Save/Publish present-but-disabled — no fabrication.
Claude-verified:
  • Build clean + JB tests 7/7 (own runs); gate logic confirmed in source; Codex Playwright evidence — gate enabled+disabled, no console errors, no horizontal scroll desktop/mobile, axe 0 violations; fixed a real mobile top-bar bug pre-handoff.
  • The 6 events-studio-shell test reds are pre-existing/unrelated (stale ES-integration tests, not touched/imported by Turn 2) — flagged as a separate Events Studio cleanup, not a JB blocker.
72c63832
Turn 3 · Phase 3 Verified on branch Built (the WYSIWYG core):
  • Editor JourneyConfig state store + live two-way binding — every settings edit re-renders the preview instantly; selecting a step in the strip jumps the preview and syncs ?step=; document.title tracks the journey name; device toggle (desktop/mobile preview).
  • All 7 §3 per-stage settings panels + reorder / enable-disable with core-step + confirmation-last guards (can't remove or mis-order required stages). Custom text + branding/footer. Still in-memory — Save/Publish honestly disabled until Phase 4.
Claude-verified (own runs):
  • Build clean (runtime + builder + events-studio-shell), JB tests 7/7; confirmed the binding is genuinely wired (config store → updateStepnormalizeOrder → preview-from-config, not a static mock). Codex Playwright acceptance green after fixing real a11y/contrast + selector/timing issues.
1cb82688
Turn 4 · Phase 4 Verified on branch Built (Save/Publish made real):
  • journey_surfaces D1 table + migration (partial unique index → at most one live per (tenant,journey)); seven v1/journeys endpoints (list/create/read/save/delete/publish + public read) on the events worker; draft versioning; atomic single-live publish via batch([demote, promote]).
  • Editor + /journeys list wired to real API data (create, live/drafts, duplicate-as-draft, remote read-back, debounced autosave, explicit Save, Publish confirmation). localStorage is an honest offline fallback only — visibly distinct from a saved draft, Publish gated on API availability.
  • Parameterized queries, signed-context tenant scoping, requireRole admin gate, audit capture. SDK-friendly camelCase DTOs + paginated list (page/limit/total/hasMore).
Claude-verified (own runs):
  • Re-ran the round-trip test myself — drove the real endpoints (publish 200 · public read 200 · list 200 · cross-tenant 404) and asserts the terminal stored state: exactly one is_live, prior version demoted, public serves the latest published, cross-tenant denied. journey-runtime 7/7; 3 typechecks clean. Read the route + editor to confirm atomicity, tenant scoping from signed session, and non-faked Save/Publish.
06e290b9
Turn 5 · Phase 5 Verified on branch Built (public render + input capture):
  • PublicJourneyRoute at /public/journeys/:slug (unauthenticated public set), consuming GET /v1/journeys/public/:slug; rendered via a shared JourneyStepPage in @voyage/journey-runtime so the public flow and the editor preview are byte-identical. A React-free @voyage/journey-runtime/core entry keeps the worker bundle lean.
  • Sequential branded steps, ?step= deep-link/back/refresh state, per-step document.title; submits only on confirmation; success shown only after a read-back-verified persisted registration. Public endpoints added: visit · register · read-back (registrations/:guestId?email=).
  • Real capture via the existing createEventsStudioRegistrationForTenant; a non-event-bound journey returns 409 JOURNEY_EVENT_REQUIRED — honestly incomplete, never a fake confirmation.
Claude-verified (own runs):
  • Re-ran the worker round-trip (publish→visit→register→read-back, asserts persisted guest row + registration_count=1 + custom-field round-trip), the jsdom wizard submit-to-success, journey-runtime 7/7, and all 4 typechecks. Read the code: success is unreachable without a server-verified persisted record; read-back is tenant+event+guest+email scoped + parameterized. Codex Playwright smoke: desktop+mobile axe 0, no horizontal overflow.
5b15f679
Turn 6 · Phase 0 In flight Building (access foundation — two-layer gating, both tiers):
  • Layer 1 — control-plane feature_catalog flag journey_builder (off → nav hidden + routes redirect + /v1/journeys* API rejects).
  • Layer 2 — dedicated journey.build capability via requireRole/hasCapability, enforced at the app route-guard AND the API; Designer = custom role off the administrator prefab; SAML JIT configurable attribute mapping; conflict UX = hide+redirect, never error. Public routes stay open.
Acceptance: the full matrix — (flag on/off) × (has/lacks journey.build) at both tiers; a hand-crafted API request without the capability is rejected (UI-hide is not the only control). Identity-stack bar (audit, parameterized, no enumeration). Claude exercises the deny paths at the API tier before accepting.

Design & scope

Canonical engineering spec committed in-repo at docs/journey-builder/engineering-spec.html (from the Claude Design handoff + the Journey Builder.fig prototype). Access model is firm; four naming/SSO defaults are PM/Platform calls.

Access model (two layers): Out of scope (v1):

Future workstream — programmatic path (post-v1) Planned

Operator direction (2026-06-04): the WYSIWYG builder is one consumer of the Voyage journey/registration APIs. Anyone — including Claude — should be able to custom-build their own journey / event-registration portal against the same backend APIs. Queued as a follow-on after the core builder (not in the current v1 phases).

★ Foundation — Developer auth & credentialing plane (operator, 2026-06-04):

Before any SDK or custom app can call a Voyage tenant, there must be a first-class way to mint and manage credentials. This is Voyage's responsibility to build once and document — not something each integrator reinvents. It is a prerequisite for the SDK/reference-app deliverables below.

Consumer deliverables (build on the foundation above): Same APIs power both paths: the in-UI WYSIWYG builder AND fully custom code. The API surface hardened in Phases 4–5 becomes the documented public contract this workstream builds on; the auth/credentialing plane is what makes that contract callable from outside the platform.