From author prompt
to the signed-in app.
How selected DS APIs become a generated step, how a publication chooses a visitor's path, and what signup hands to the app.
Character-prefill transfer remains pending. General can open Explore or the character chooser. Expose funnels only after characters-v2 reaches 100% rollout.
Who owns each handoff?
Follow a flow
Choose a scenario, then move through each handoff.
Architecture and operator reference
Who owns each layer?Architecture
| Layer | Responsibility | Main implementation |
|---|---|---|
| Recipe catalog | Published recipes, families and anonymous member selection | Sirloin character recipe services and storage |
| Parser | Project supported creation inputs and pin their origin | funnelparser.go: parseSources, parseFunnelRecipe |
| Builder | Edit presentation, ordering and enabled steps | Flank funnel editor; authenticated server functions |
| Step Studio | Bounded generation, compilation and preview; private conversation UI | Flank step-studio server helpers and custom-step components |
| Step library and jobs | Immutable revisions, owner-scoped sessions, leases and replay | Sirloin studioservices.go and storage/studio*.go |
| Persistence | Revisions, request replay, drafts and version snapshots | storage/funnel.go |
| Public delivery | Published-only access, strict-mode projection and media signing | GetPublishedFunnel |
| Visitor runtime | Branch selection, controls, photo checks and local progress | Brisket funnel state machine and renderer |
| Signup attribution | Record conversion before account-dependent initialization | Brisket server attribution and Sirloin tracking storage |
Follow the system map and visitor walkthrough above for this cross-service flow.
Character funnels currently fall back to Explore with an explicit funnel/publication reference. The separate character adapter will consume compatible saved inputs; /hop does not create a character or mark recipe stages complete. General funnels go to Explore or the existing RI/VI character chooser according to their configured destination, without character-prefill handoff. New character-creation deep links belong to V3. Source: continuation routes.
How does Step Studio generate and save a step?Architecture
Each Studio opening starts a fresh conversation. There is no saved-session browser; authors keep reusable work by adding it to a funnel draft or the shared step library. An authenticated Flank editor still creates a private backend session to coordinate generation and idempotent mutations. The session document stores conversation turns, component selection, author media, settings, layout, text overrides, title and the selected library revision, including authoring choices made before the first prompt. The last successful built result is stored separately with its job ID. Mutations use request IDs and expected session revisions. A lost admission response retains the exact request for reconnecting; a later rejection before admission does not erase that uncertainty. Polling only reads job state and never invokes a model.
Generation admits one active job per author and session in Sirloin. The worker claims a fenced 90-second lease, renews it every 30 seconds, and stops when ownership is lost. The UI waits three seconds after each response before polling again, so slow reads cannot overlap. An abandoned queued job or expired worker becomes interrupted; a process restart does not automatically repeat a possibly charged model request. Polling within the open Studio retrieves the same terminal result. Closing Studio discards the conversation UI; it does not cancel an already admitted backend job or provide a way to reopen it. Retrying is an explicit new author request, and a failed attempt leaves the last successful result intact.
The entire generation has a ten-minute deadline, including asset signing, model requests, compilation and up to three total generation/repair attempts. Each provider request is capped at seven minutes. Recent conversation is bounded to eight turns and 16,000 UTF-8 bytes; older decisions use a bounded summary. Requests bound prompt, context, source, assets and response size, and send an explicit output-token allowance. Current limits are 256 KiB of TSX plus authored CSS, 2 MiB of bundled JS plus CSS, 12 generation assets and 16 KiB of settings. Sources: request contracts, generation, worker, durable jobs.
The component picker lists all 33 public Foxy DS UI entrypoints, one per row with a rendered DS example and 11 selected by default. Search matches names and descriptions without changing the selected imports. Menu, select and tooltip examples show their triggers so preview overlays cannot cover the editor. Selecting Icon also makes the shared icon artwork available. Only selected component APIs are included in the prompt and accepted by the build. React, local ./step.css and motion/react are also supported. Generated CSS cannot import external resources or filesystem files. Motion instructions require reduced-motion behavior. Flank compiles and repairs before showing a result, then recompiles the exact source and selected imports when saving a library revision; the browser's supplied bundle is not trusted. Sources: prompt, build, revision save.
What exactly goes to OpenRouter?Architecture
Flank sends a chat-completions request from the server. The browser does not receive FLANK_OPENROUTER_API_KEY. The model comes from FLANK_STEP_STUDIO_MODEL, falling back to STEP_DEFAULT_MODEL; the current default is anthropic/claude-sonnet-5.5. The request sets temperature: 0.4 and max_tokens: 24000. Source: provider request and message assembly.
Messages appear in this order:
| Message | Contents |
|---|---|
| System | Runtime/import restrictions, StepProps, answer and navigation contracts, written design rules, selected DS declarations, and the required response format. |
| Older decisions, when present | The supplied conversation summary, or bounded excerpts of older turns. Building this summary does not call another model. |
| Recent conversation | At most eight user/assistant turns, with a combined 16,000 UTF-8 byte limit. |
| Current author request | FUNNEL CONTEXT, current TSX/CSS when refining, an explicitly supplied preview error, and AUTHOR REQUEST. Context contains eligible recipe fields, finalized author media, instance settings and layout. |
| Repair pair, after a failed attempt | The latest candidate response, capped at 80,000 characters, followed by validation/build errors capped at 4,000 characters. Earlier repair pairs are replaced. |
The prompt requests three fenced blocks: JSON metadata, complete TSX, and complete CSS. The parser requires JSON and TSX, accepts omitted CSS as empty, and extracts the first matching blocks. Metadata declares the title, summary, recipe reads/writes, generic outputs and allowed interaction events. Parsing and compilation must succeed before the result replaces the working preview. The three-attempt repair loop handles metadata/build failures; provider HTTP errors, invalid provider responses, timeouts and missing credentials fail immediately. Sources: system prompt, reply parser.
The system prompt does not include DS implementation files, stylesheets, Storybook pages or picker screenshots. Generation receives no visitor answers, visitor photos, signup credentials or private recipe execution stages. Author-supplied prompt/history and attached author-media URLs do cross the provider boundary. Every message is text; media URLs are JSON strings, without multimodal image/video blocks. The model is instructed to refer to media by context name/type rather than hard-code expiring URLs. Full chat messages are limited to 384 KiB before each provider call; the provider response is limited to 768 KiB. DS declaration context has its own 256 KiB ceiling. Exceeding a limit fails explicitly. Sources: request schema, context limits.
How do DS components reach the prompt and runtime?Architecture
The author selects component groups in Components. STEP_DS_CATALOG defines the picker and accepted IDs; STEP_DS_COMPONENTS defines the eleven initial selections. Selecting button, for example, permits @foxy/ds/button and includes that entry's exported names, props, allowed variants and declaration comments. This teaches the model how to use the existing component. It does not ask the model to recreate Button. Sources: catalog and defaults, picker.
dsComponentTypes resolves each selected entry's types path from the built @foxy/ds/package.json and follows referenced local .d.ts files. Select and Dropdown Menu also include their explicitly registered Radix prop declarations. Supporting declarations do not grant additional imports. Selecting Icon adds @foxy/ds/icons and its artwork names; other components receive the shared icon type without that artwork list. The DESIGN instructions name theme, color, spacing, radius and shadow tokens manually. Source: declaration loader and design instructions.
After generation, the compiler bundles the step with React, the SDK, imported DS components and their CSS, plus Motion when used. Selection authorizes imports; unused components are not all bundled. The declarations guide generation; esbuild compiles TSX without checking every component prop type. Preview remains a separate check. The shared token stylesheet and font are included even when no components are selected. Published artifacts retain this compiled result, so a later DS deployment does not restyle an existing revision automatically. Sources: compiler, DS build in Flank deployment.
| DS change | Studio maintenance |
|---|---|
| Existing component gains props or variants | Rebuild DS and deploy Flank. The loader reads the new declarations for future generation; no copied prop documentation needs updating. Review preview examples if their usage changes. |
| New public component group | Add its ID/title/description to STEP_DS_CATALOG, a real STUDIO_COMPONENT_PREVIEWS example, and its ID plus the component-count limit in Sirloin validation. Add it to STEP_DS_COMPONENTS only if it should start selected. |
| New icon artwork | Rebuild DS and deploy Flank. Future prompts that select Icon read the new exported artwork names automatically. |
| New tokens or design conventions | Rebuild for runtime CSS, and update the written DESIGN guidance when the generator must know the new names or rules. |
| New external prop-type dependency | Check whether its declarations are included. The loader follows local DS references; extra external packages need explicit registration, as Radix does today. |
| New DS peer dependency | Include it in Flank's runtime dependencies so deployed compilation can resolve it. The catalog tests check this dependency boundary. |
Catalog tests compare public DS exports, picker IDs and Sirloin's allowlist/count. Prompt tests check selected and supporting types. Run them with compiler tests after a catalog change; rebuilding DS alone does not register a new component in Studio.
What do the Studio database records mean?Architecture
The migration separates private authoring state from shared reusable steps. Here, canonical_* columns contain complete protobuf-JSON messages stored as bytes. The name does not mean these payloads undergo an extra canonicalization pass. SQL columns expose IDs, status and timing for constraints and lookups. Sources: migration, session serialization, job serialization.
| Record | Purpose |
|---|---|
studio_sessions |
An author's current document and last successful result. Its increasing revision detects edits based on stale state. The (owner_id, id) index supports owner-scoped listing; the primary key separately identifies one session. |
studio_jobs |
One admitted generation job, which can contain up to three model attempts, linked to its session and owner. Request IDs prevent duplicate admission; a request hash identifies the submitted contents. Status records queued/running/terminal state. |
| Job worker, fence and times | worker_id identifies the process. fence is a claim version checked on heartbeat/finish. lease_expires_at requires renewed ownership; deadline_at caps the whole attempt. Unique partial indexes enforce one active job per owner and session. |
studio_requests |
Mutation replay: the owner/request ID, request fingerprint and original response allow an identical retry without repeating the mutation. Reusing an ID for different input is rejected. |
step_library, step_revisions, assets and revision-asset links |
Shared item metadata, immutable reusable snapshots, finalized media and retained references. A revision's digest identifies its saved contents. |
How do card previews, layout and text edits work?Architecture
Builder cards load the exact customStepRevision pin, verify its library/revision IDs and digest, then render its built artifact through the existing sandbox and scaled preview viewport. Declared recipe fields and author assets use signed media; settings and copy come from the funnel instance. Cards mount near the viewport, ignore late responses after a reference changes, and never commit answers or analytics. Studio and cards share the same preview shell. Sources: card loader and context, card preview.
props.customStep.layout holds optional hideBack, hideProgress, hideSkip and fullScreen booleans. Missing or false values preserve the funnel's settings. Full screen removes the host heading and padded content shell; Back/progress become overlays when enabled, and Skip is visible unless Hide Skip is enabled. Hiding Skip does not make the step required: failures still advance automatically. props.customStep.textOverrides maps text IDs to { original, value }. These values belong to the funnel instance, not its library artifact; publication pins them with the rest of the funnel. Sources: layout controls, preview shell, visitor shell.
The trusted Studio overlay provides Edit text → pencil → Apply/Reset without calling the model. A host-injected adapter changes plain HTML/SVG text nodes inside the sandbox, including older artifacts; it does not edit images, videos, canvas, markup or answer values. IDs combine the original text with its DOM location, and an override also checks the original before applying. Regeneration or dynamic content can invalidate that match. Text and target bounds are checked on both sides of the frame; Sirloin validates instance props before saving. Sources: text adapter, instance validation.
What crosses the custom-step boundary?Architecture
Each custom step has a distinct custom:<uuid> instance ID, an immutable customStepRevision pin containing library/revision IDs, digest and manifest, and per-instance settings. The manifest declares recipe fields it reads, recipe keys it writes, named events, selected components and typed generic outputs. Recipe writes must also be declared reads and must refer to non-photo fields in that branch. Generic output keys are separate from recipe writes; reserved prototype keys are rejected. Generic outputs support strings, numbers, booleans and string arrays, with optional required flags and fixed choices. Recipe writes and generic outputs together may declare at most 32 answer keys. Payloads permit only bounded flat values: strings up to 500 characters, arrays up to 32 items and 16 KiB per message. Recipe option values preserve scalar, null and flat-array semantics.
Brisket loads the published artifact and runs it in an opaque-origin iframe with sandbox="allow-scripts". The host transfers a private MessageChannel after the frame reports loaded, bounds and validates messages, and enforces a ready timeout. CSP permits the exact bundled script/style and declared media origins, with fetch connections and form submission disabled. The frame receives declared non-photo field metadata, author assets, settings, theme and saved values. It receives no visitor photos, cookies or parent DOM access. Sources: frame contract, runtime.
The SDK exposes change, complete, track, back and skip. These are requests to the host; generated code does not choose funnel URLs, signup routes or branch transitions. The host validates the complete proposed recipe write before committing it, applies Back only when allowed, and owns Continue and Skip. Every custom step is optional; load, ready or runtime failure reports a bounded error and advances past the step. An input or promo-code step captures a local value only; it does not redeem a code, alter a price or create a payment integration.
How are library pins and author assets retained?Architecture
Saving creates an immutable FunnelStepRevision with source, built artifact, manifest and asset references. Adopting that revision changes only a funnel draft. Existing instances and publications retain their prior pins until an editor explicitly adopts another revision and publishes. Public artifact requests require a currently published funnel, identify the requested publication version and step instance, verify the pin/digest, and return built JS/CSS, manifest and signed author media. An explicitly requested retained historical publication can still serve while that funnel remains published. Author conversation and source stay private. Archiving a library item hides it from the active library; it does not delete revisions needed by publications. Sources: public artifact delivery, library storage, protobuf contract.
Author images/videos upload to a staging key and are finalized after owner, byte count and media-type checks. Finalization creates a new final key; a staging upload URL cannot overwrite a published asset. Stable storage-media:recipe-media/funnel-assets/... references and asset IDs are retained with revisions. After the database confirms the immutable winning key, finalization best-effort deletes its staging object and any losing upload from that request within a five-second cleanup budget. A repeated finalize retries staging cleanup; deletion failures are logged with asset and object IDs. An uncertain database result retains the upload unless a finalized row proves which key won; failed losing-object cleanup and unresolved writes require reconciliation rather than risking a retained revision. Pending deployment configuration: apply a seven-day object expiry only to the studio-staging/ prefix, beyond the existing 72-hour PUT URL expiry, to cover abandoned uploads, failed cleanup and staging rewrites. This rule has not been configured or verified by this change. Never apply that expiry to recipe-media/funnel-assets/, whose objects may be retained by immutable revisions. Preview, generation and public delivery obtain fresh signed URLs instead of persisting expiring URLs. Shared-library refinement may reuse only assets retained by the requested immutable revision; unrelated newly uploaded assets remain owner-scoped. Supported uploads are JPEG, PNG, WebP, AVIF, MP4 and WebM, up to 50 MiB each. Visitor photos remain exclusively in the existing recipe photo flow. Sources: asset finalization, generation asset authorization.
Migration 189_funnel_studio.sql adds private sessions/jobs/replays, shared library revisions, finalized assets and revision-asset retention. Apply main’s migrations through 188 first, then 189, and deploy matching Sirloin, Flank and Brisket contracts. Previews that already applied Studio as migration 185 or 186 need schema and migration-history reconciliation before startup: main uses 185 for renewal payment operations and 186 for character-chain retries. Apply missing main migrations and reconcile the existing Studio tables with 189; do not advance the recorded version alone or discard existing data.
Where does Flank render a step?Architecture
The funnel route loads the registry and composes the toolbar, table and dialogs. It keeps the server-function imports and passes them to useFunnelEditor, which coordinates editor state and the action, catalog, source and autosave hooks.
The steps component arranges branch steps. The step editor exposes editing and preview controls. Rendering is split into:
- step-preview/index.tsx: preview state and screen composition.
- step-preview/content.tsx: choice, text, signup and photo control selection, using DS components directly.
- step-preview/photo.tsx: photo preview states.
Shared pure functions live in lib/funnel/helper-functions.ts, with recipe media prefixes, known field/property keys, step kinds and copy-path rules in the sibling constants.ts. The visitor journey has a separate Brisket renderer; Flank previews do not run signup or record visitor conversions.
Signup presentation and the app’s authentication pages share SpiralBackground from @foxy/ds/spiral-background. It is a decorative component usable outside authentication, with light/dark colors and reduced-motion support. Flank includes @foxy/ds/spiral-background.css in its isolated preview stylesheet.
Brisket's session renderer composes the state machine, persistence and screen effects. Its sibling use-unsaved-continuation.ts carries the continue-without-inputs decision across the authentication remount; use-signup-departure.ts handles the final save, attribution preparation and navigation. The signup component owns email, verification and sign-in handlers. Fixed redirect destinations come from URLS in src/lib/constants.ts; continuation/helper-functions.ts parses the explicit funnel/publication reference and builds URLs with query parameters.
How are sources resolved?Architecture
- Choose one RI source, one VI source, or one of each. Each source specifies exactly one
routeKeyorfamilyKey. - Both synthesizes a
character_typeentry step. Direct recipes cannot carry family selector settings. - A family must be published. Funnel authors can select published families even when they are disabled or hidden in the character catalog; using one in a funnel does not enable it there. Draft-only families are excluded. Every selector must be either asked or fixed, exactly once. Asked selectors become option-grid entry steps; fixed values must belong to the selector.
- The parser expands selector combinations into branch candidates. Each tuple must resolve to exactly one onboarding member, or exactly one default member when no onboarding member matches. It does not perform account-based or weighted allocation.
- Each selected route must have a published recipe matching the source's character type. Each branch pins
routeKey,versionId, recipedigest, and, for a family source,familyKeyandfamilyDigest.
Any reported issue rejects the source rebuild without replacing the existing draft. Examples include ambiguous family members, unpublished families, selector/field key collisions, and character-type mismatches.
How does a recipe become steps?Architecture
parseFunnelRecipe locates presentation.creationFlowKey and projects that flow. It does not walk arbitrary recipe flows or execute transitions.
| Recipe element | Funnel result |
|---|---|
| Unsubscribed-only stage | Stage ID recorded in paywallStageIds; not rendered anonymously |
| Other non-everyone visibility | Unsupported-visibility issue |
Hidden field (props.hidden) |
Omitted |
| Form/upload field, or text control in another stage | Candidate input; first occurrence of each field key wins |
| Option grid, chips, color picker, multi-select, text | field step, ID field:<key>, cloned field contract and original stageId |
| Media upload | photos step with the same field contract |
| Everyone-visible stage with no projected fields | Stage ID recorded in sessionStageIds |
| Conditional projected stage/field, unsupported control, or content blocks on a projected stage | Issue; rebuild rejected |
Session and paywall stage IDs are metadata. They are not executable stage objects in the public funnel.
Photo steps use stage copy and the primary action's label. The parser copies confirmation presentation from the first stage whose props.sourceFieldKey matches the photo field, including action labels. Copying an action label does not execute that action or its validation workflow.
What is validated?Architecture
The parser validates the supported field-rule shape and public media references. Reserved keys such as character_type, __proto__, constructor, and prototype cannot become recipe answer fields. Authenticated media paths cannot be exposed in the public definition.
Photo contracts require contentTypes, maxBytes, minWidth, minHeight, and maxPixels; count bounds are optional. Brisket uses those rules through the existing character-upload validation helpers. Missing or malformed required policy is rejected, not replaced with an invented browser default. Omitted counts default to one minimum and a maximum equal to that minimum.
HEIC/HEIF, AVIF, and WebP conversion is a browser input convenience when the recipe allows JPEG. Brisket’s shared src/lib/image-upload/helper-functions.ts converter serves both Create and the funnel: browser decoding runs first, with a dynamically loaded HEIC decoder as fallback. WebP is kept unchanged when the recipe accepts it. Conversion input has a 10 MiB safety cap; the converted JPEG must satisfy the recipe's byte and dimension limits. The recipe remains authoritative; missing or unsafe policies are rejected rather than replaced with a fixed size allowance. The checking/accepted screen only reflects local file checks. It is not server-side identity, adult-likeness, or workflow approval.
What can an editor change?Architecture
An editor can change supported copy, reorder or disable branch steps, configure choice randomization and presentation settings, and maintain quick links. Field semantics, source pins and branch structure remain protected. Entry and signup steps cannot be dropped. Changing the underlying source requires a source rebuild.
Strict mode defaults on and applies only to RI. Face & Chest remains available; other RI settings and photo steps are locked in the builder and dropped from the public projection. Their saved configuration is retained. VI, routing and signup remain available. Disabling strict mode unlocks RI editing; save and publish applies it to visitors.
General funnels contain ordered custom entry steps and native signup, with no recipe sources or branches. A custom interaction does not rewrite native recipe definitions. In Character funnels it may use declared non-photo fields, including fields whose native presentation was dropped, while publication validation preserves the pinned recipe contract.
Why do pins and publications matter?Architecture
A draft is an editable funnel, a publication is a saved visitor definition, and a recipe pin identifies the recipe version used to build a branch. They are separate version boundaries.
Publishing clones the saved draft into a new funnel version. It does not silently reparse newer recipes. Public delivery reads the stored publication, applies strict mode and presentation overrides, and signs allowed media slots. Later draft edits do not alter that publication. Unpublishing or archiving disables public access; reactivation returns to draft and requires another publish.
Sirloin stores canonical protobuf JSON and SHA-256 digests in onboarding.funnels and onboarding.funnel_versions. Storage writes published versions by insertion. A 32-bit revision counter increments on each saved mutation and protects concurrent edits; it is separate from the published version counter. Flank passes revisions as JavaScript numbers. Request replay is scoped by the editor user ID (funnel_requests.user_id). No custom SQL function or trigger is required by the current migrations. Existing preview databases can retain earlier triggers until explicitly reconciled; see the migration notes.
Migration 179_funnels.sql creates storage, tracking and profile attribution together, after main’s migration 178. Its revision, published version and conversion columns are INTEGER; impressions remain BIGINT. Existing stored string revisions and request replay hashes remain readable.
Preview databases that already applied an earlier funnel migration 173, 174, 175 or 177 need schema and migration-history reconciliation. These numbers now belong to main. Confirm that main’s migrations 173 through 178 have run, and reconcile the existing funnel schema with migration 179 before startup. Advancing the recorded version alone can skip main’s changes; running 179 against existing funnel tables fails. Rename the request-replay column from account_id to user_id before deploying the updated queries. Check existing revision, published version and conversion values before converting their old BIGINT columns to INTEGER; preserve funnel data and publication history. No hosted database is reset automatically. After migration 179, profile foreign-key validation runs separately as described in the migration notes.
How does the visitor's route get selected?Architecture
activeSteps builds the ordered route used by Next and Back. It is the eligible path, including answered steps, rather than a list of remaining questions. Character funnels resolve a source first: one source selects automatically; two sources use the valid character_type chooser answer. A branch matches that source's type and all selector values, taking a fixed source selector before a visitor answer. Source: source, branch and route helpers.
| Condition | Why the entry step remains |
|---|---|
step.kind === "character_type" |
The visitor needs the chooser before a source exists. This is the step kind and answer key, not a type restriction. |
Custom step without props.characterType |
A shared landing or intro can run before selection. |
Source selected, no props.characterType |
An ordinary shared entry can run after source selection. |
Source selected, matching props.characterType |
A type-specific entry belongs to the selected real/virtual source. |
The helper appends the selected branch's steps, then signup only when that branch exists. It removes dropped steps and failing field showWhen conditions. General funnels take the shorter path: ordered entry steps plus signup, removing dropped steps without source/branch resolution. Their branch-specific field conditions do not apply. Runtime filtering does not authorize authoring unsupported definitions; Sirloin still validates step placement and custom-step props before publication.
Native recipe answers and photos belong to the selected branch. Custom change values stay under the custom instance ID. On custom completion, the host validates the full proposal before accepting declared non-photo recipe writes; generic outputs remain instance-local. General steps cannot declare recipe writes. Changing source or branch removes incompatible recipe answers and photos. Sources: custom answer validation, answer transitions, progress mapping.
What is saved for character creation?Architecture
Brisket stores one record per funnel in IndexedDB database foxy-onboarding-funnels, database version 1, store progress, key funnelId. The record schema is 2; changing record properties does not require a database upgrade. Files stay as File[], without base64 conversion or a second copy in localStorage.
type FunnelProgress = {
schemaVersion: 2;
funnelId: string;
version: string; // Answered publication, not a recipe version.
ownerUserId: string | null;
expiresAt: number;
writeToken: string;
stepId: string | null;
screen: "answering" | "verifyingCode";
characterType: "RI" | "VI" | null;
gender: "female" | "male" | null;
recipe: {
routeKey: string;
versionId: string;
digest: string;
steps: JsonValue[];
} | null;
stages: Record<string, Record<string, JsonValue>>;
customInputs?: Record<string, Record<string, JsonValue>>;
filesByStage: Record<string, Record<string, File[]>>;
};
recipe, characterType, gender, stages and filesByStage are the character inputs. They are saved as the visitor progresses, before signup. The other fields select the right record, resume the screen and prevent cross-account or stale writes. Recipe IDs, stage IDs, field keys and native values remain unchanged: for example, stages.questionnaire.age = "25-29" or filesByStage["upload-face-create"].face_chest = [file]. These are fixture-based illustrations, not captured production records.
V2 adds optional customInputs[instanceId] for generic answers and in-progress custom interaction values. The record remains schema 2 and IndexedDB version 1. Generic inputs never become recipe stage values automatically. Accepted recipe writes still map to their original native stage and field; visitor photos remain File[]. Repeated uses of the same library revision have distinct instance keys. A General record has recipe, character type and gender set to null, empty recipe stages/files, and only its own instance inputs. These generic values are not handed to the character adapter. Source: progress mapping.
The recipe snapshot is the selected branch's public projection, not its private workflow or billing instructions. routeKey, versionId and digest pin the exact native recipe for the later adapter. There are no duplicate answer maps, photo-binding indices or stage-completion flags. RI/VI and gender are checked against the matching publication. Fixed recipe gender is carried by FunnelBranch.genderConstraint; unknown gender stays null. A family selector uses its original key, even when it is not named gender.
Changing type or gender replaces the recipe and incompatible inputs together. Validation uses stable identities and value semantics; renewed signed media URLs do not invalidate a saved pin. A different publication, unavailable source or unsupported record schema never silently repins old answers. /hop retains recoverable data and reaches Explore. Anonymous /f entry atomically clears unusable anonymous progress from an older publication or a recognized legacy format, including expired or malformed drafts, before starting fresh. It preserves account-owned records, unknown future formats and records from a newer publication than the reader. Storage failure is not treated as corrupt data. A failed restore starts the funnel with empty in-memory inputs, without a recovery screen or overwriting the protected record. A failed final save continues signup without saved inputs: Character uses Explore, while General keeps its configured destination.
The seven-day expiry renews only on edits. Claim, save, reset and consume operations check ownership and the observed writeToken inside the IndexedDB transaction. Every successful write changes that token. A stale tab cannot overwrite a newer record; a delayed adapter cannot delete newer answers. No arbitrary “latest funnel” lookup is used.
Storage responsibilities are separate: progress mapping builds the stored record from form inputs and restores those inputs; publication validation checks compatibility with the pinned funnel. Progress comparison provides shared equality helpers. IndexedDB transactions remain in the storage helpers.
The character adapter is pending as TODO(FOXY-1546) in /hop. Its intended reader API is readOwnedFunnelProgress(reference, userId, publication, signal?); after durable application through the character draft pipeline, it can call consumeFunnelProgress(reference, userId, writeToken). These storage APIs exist, but /hop does not transfer inputs or create a character today. Entering Explore does not consume anything. Authenticated character storage (characters-v2-recipe-drafts) and the existing VI localStorage prefill bridge remain separate. Sources: current hop, stored type, transaction helpers, and recipe projection.
How do source editing and history differ?Architecture
Flank shows a saved-draft recipe-status chip in the table only for updates or an unavailable recipe, alongside Edit source. The editor header also shows current recipes in green and loading, missing-source or failed-check states in neutral. “Unavailable” includes deleted, unpublished and invalid sources. Recipe details provides status, diagnostics, rechecking and access to source editing. The chip does not describe whether draft edits have been published.
Source editing rebuilds the draft from explicitly selected sources through SetFunnelSources. Rebuilding discards step customizations. The reviewed source-refresh workflow, edit reconciliation, refresh undo and their RPCs have been removed. Source inspection remains read-only and returns statuses and issues only.
The checkpoint-table migration has also been removed. This does not drop tables in databases where it previously ran or rewrite draft, publication or request-replay JSON.
The editor’s top-right Viewing dropdown switches between the editable draft and a read-only publication in the main view. Browsing preserves unsaved draft edits. Publication mode hides navigation and quick links, prevents step changes, and offers return-to-draft or confirmed restoration in the bottom panel.
History reads immutable rows in onboarding.funnel_versions by funnel ID and version. Restore clones a stored definition and validates its exact recipe pins, without reparsing today's source or requiring today's family digest. Restoration preserves quick links, counters and the live publication. A later explicit publish creates the next version number. See source rebuild, history, and transactional storage.
How are new fields deployed?Architecture
FunnelDefinition.continuation (field 10) describes type and destination. FunnelBranch.genderConstraint (field 8) carries source-owned gender. The deprecated post_signup_redirect descriptor (field 4) remains readable with a safe /explore value; new clients do not offer arbitrary URLs.
Sirloin canonical-row and replay readers and Flank's protobuf JSON reader reject unknown fields. Deploy matching schema-aware Sirloin, Flank and Brisket releases before authoring or sharing funnels. Remove the refresh UI and server-function calls before retiring the Sirloin endpoints. Reload old editor tabs; old inspection responses may contain the removed preview or checkpoint fields and fail strict decoding. Drain older instances and reload authoring and public-preview tabs; a new client rejects an old server’s JSON response if it still includes the removed capability field. There is no runtime rollout flag: editing, version restoration, public rendering and progress saving work immediately.
After new fields or schema-2 browser records exist, the schema-aware release is the rollback floor. Revert feature behavior while retaining descriptors, strict JSON readers, storage shape and ownership/token checks. Do not globally discard unknown JSON or rewrite immutable publications for an older binary. Removing field 4 later requires its own persisted-JSON compatibility plan.
The unused capability fields have been removed from API responses. Replay rows contain Funnel snapshots, not response wrappers, so this removal does not change stored JSON. Version restoration still requires author permissions, confirmation and a matching revision; changing the draft never publishes it.
What happens at signup?Architecture
The Events dialog in the table derives a reference from the live publication, or the current draft when unpublished. It mirrors public step eligibility, including RI strict mode, and lists funnel_step_viewed, funnel_step_completed and custom skip/error/interaction events with copyable filters. Both use stable step IDs; branch-owned steps also include branch_id. Completion records the selected recipe option value or photo count only when advancing succeeds. Draft filters omit the publication version. See events and dashboard filters for payloads and counting rules.
Copy dashboard instructions includes a vertical step-impressions column chart: every eligible step in journey order, readable labels, unique-user counts and zero-count steps retained. Keep branch paths and publication versions separate. The prompt also requests an ordered funnel for required milestones, with drop-off counts and percentages from the previous step, counting entrants at the first step so restored visits do not inflate downstream reach. These client events describe step activity; the server conversion ledger remains authoritative for signup totals. Internal states such as answering and checkingPhoto are not separate analytics events. funnel_step_completed can include valid declared choices in answer, photo_count, or skipped: true; it excludes typed text, file contents and credentials, and signup emits a view only.
An opening event records an impression. Signup preparation stores the recorded visit in an HTTP-only cookie. After authentication, server attribution verifies the identity and signup timing, then atomically records the first eligible conversion and users.profile.funnel_id before account-dependent initialization. PostHog consumes that profile value; it does not own attribution.
There are three distinct failure boundaries:
| Boundary | Current behavior |
|---|---|
| Final local save | Continue signup without saved inputs. Character uses Explore; General keeps its configured Explore or character chooser destination. |
| Pre-signup attribution preparation | useFunnelOpening retries the preparation mutation twice. If preparation still rejects, useSignupDeparture reports a best-effort funnel_signup_preparation_failed browser telemetry warning and continues email signup, OAuth or sign-in. Saved inputs retain the /hop destination; failed storage uses Explore for Character or the configured destination for General. Reporting failures also cannot block departure. The warning includes the funnel ID and version, without raw errors, answers or credentials. Conversion attribution may be missing for that signup. |
| Server account initialization | A timeout or SERVICE_UNAVAILABLE failure logs, clears pending attribution and continues to Explore. Other errors propagate. |
The normal signup departure, account-initialization and /hop waiting states use the shared Foxy logo and spinner. There is no saved-progress recovery screen, storage-warning banner or departure retry screen. Actual authentication failures still show their authentication error. Cancellation and account changes stop the old departure instead of allowing late navigation. The ten-second initialization and /hop limits below do not apply to pre-signup preparation in useSignupDeparture.
Account initialization has a ten-second deadline. A timeout or transient initialization failure logs a sanitized funnel_initialization_failed warning through the server Pino/OpenTelemetry pipeline to Axiom, clears the pending attribution cookie through /api/funnel/initialization-fallback, and redirects to Explore without a message or retry screen. This prevents an initialization loop while retaining saved browser answers and photos. The cleanup endpoint needs no account data or auth lookup; Explore still requires authentication. Failed attribution may leave the conversion unrecorded. Only after initialization succeeds does /hop begin its separate ten-second budget for auth readiness, storage and publication lookup. Storage read failures continue automatically to Explore without saved inputs. Once authentication is ready, that deadline also falls back to Explore automatically. If authentication is still unavailable at the deadline, or an unexpected continuation error occurs, the loader stays visible while the browser returns to foxy.ai. This deadline limits the wait; it does not expire authentication or the saved progress, whose lifetime remains seven days. Late callbacks cannot navigate after timeout or account changes.
Already signed-in arrivals at /f go directly to Explore (/) before the funnel query, opening event or renderer starts. Authentication loading mounts no funnel content. Once an anonymous visitor has entered the funnel, completing authentication keeps the existing final-save and signup-preparation path through /hop. Email, OAuth and sign-in departures attempt the last IndexedDB transaction. If it fails, they continue automatically without saved inputs; signup attribution still runs. Auth carries only the explicit funnel/publication reference. /hop checks the server publication and resolves a fixed internal destination; the URL cannot choose an arbitrary redirect. For Character funnels, both destination choices reach Explore until the character adapter exists. General funnels also use /hop to bind usable saved inputs to the signed-in account before any publication lookup. A matching General publication selects the fixed Explore or existing chooser destination without passing a funnel reference or generic inputs to character creation. Photos and inputs remain local.
The main app layout temporarily restores its existing IntroFunnelRedirect: signed-in users without a subscription or completed quiz enter one simplified /welcome quiz and skippable paywall. Returning sign-ins are eligible too; email and OAuth callbacks keep their existing destinations. /hop resolves before the user reaches this gate. skip-post-signup-quiz ON bypasses the quiz; OFF, or an unavailable flag after the grace period, keeps it. The gate captures the current app path and query in a sanitized return_to, preserving the funnel reference through paywall close. Skipping neither marks quiz completion nor consumes saved inputs. At funnel launch, remove the main-layout gate and temporary welcome entry, skip flag and continuation helpers; there are no quiz-specific auth wrappers. See signup destinations. The app tour remains available after existing character-completion and first-purchase flows; funnel continuation does not start it automatically.
Where does the funnel hand control to the app?Architecture
Funnel type and destination are separate choices. Character means recipe-aware questions; General means a journey without a character recipe. Both expose destination configuration. General's current destination helper maps the enum to a fixed internal route; it does not accept arbitrary redirect URLs. Sources: destination editor, destination mapping.
| Visitor state | Current destination and retained data |
|---|---|
Already signed in on arrival at /f/{id} |
Explore before funnel loading, opening events or progress reads. |
| Anonymous completion with usable saved progress | Authenticate, then /hop?funnel={id}&version={version}. /hop claims the browser record for the account before fetching the matching publication. |
| Character, either configured destination | Validate owned progress and reach /?funnel={id}&version={version}. Inputs stay local for the pending adapter. No character creation or photo upload starts. |
| General → Explore | Reach /, without passing generic inputs or a funnel-prefill reference into character creation. |
| General → Character creation | Reach /characters?chooser=all. The normal chooser owns the next creation flow; generic inputs are not prefills. |
| Final save fails or progress saving is bypassed | Skip /hop: Character uses Explore, while General uses its configured destination. Attribution preparation still runs. |
/hop cannot claim progress, validate the publication or finish within its authenticated deadline |
Fall back to Explore. Failed publication lookup can retain the explicit reference; no arbitrary/latest saved funnel is substituted. |
Sources: public entry, signup destination selection, final-save fallback, ownership and publication checks.
The main-layout welcome quiz gate can still intercept the resulting app destination. It preserves the path/query in a safe return_to; subscription, completion and skip-flag rules decide whether it runs. Confirmed account-owned creation sessions are exempt, but a creation-shaped URL alone does not grant that exemption. Sources: quiz gate, authentication flow.
Launch prerequisite: expose funnels only after characters-v2 reaches 100% rollout. This is the launch constraint agreed for this release, not a new guard added by the funnel runtime. The character route still selects the legacy or new flow using its existing flag. Keep General's configurable destination for future destinations; enabling it does not implement the pending Character adapter. Source: flag-dependent character route.
How should this system be extended?Architecture
This parser is character-recipe-aware: it uses CharacterControlKind, character stages and recipe pins. The funnel definition is separately stored, but the parser is not a general flow engine. A new domain should supply an explicit adapter and renderer contract rather than pretending its stages are character stages.
For a new character control or rule, update parser support, server edit validation and the Brisket control/validation path together. Test source rejection, branch selection, publication isolation, progress restoration and the user-facing behavior. Keep execution-only stages behind the authenticated character runtime.
Source and verification mapArchitecture
funnelparser_test.go: projection, sources, pins and unsupported recipe inputs.funneltracking_test.go(migration): metric defaults, history and profile foreign key.funnelintegration_test.go(storage): publication isolation, replay and concurrent revisions.- Brisket funnel recipe/storage/photo tests: snapshot, restore and file-policy behavior.
- Brisket renderer/signup, initialization/fallback and hop tests: final-save fallback, departure preparation, cookie cleanup, deadlines and cancellation.
- Flank funnel editor tests: supported authoring changes and archived-state behavior.
- Flank Studio tests: provider mocks, import restrictions, repair/deadline limits, lease loss, repeated job reads and owner-scoped assets.
- Sirloin Studio tests: SQL session/job replay, immutable revisions, publication artifact access and asset finalization.
- Brisket custom-step tests: host messages, recipe/generic validation, per-instance state and failure navigation.
These are verification entrypoints, not evidence of a live provider request, browser session, deployment or end-to-end rollout. Production builds and focused tests should be reported with the delivery evidence; live generation and browser journeys require their own explicit witness.
The interactive guide is generated from this architecture page and the operator flow. It embeds a working-tree source snapshot with a SHA-256 digest per file. Modified source files are labeled as local changes relative to the recorded base commit. Live repository links can differ from that snapshot; local changes are not presented as shipped code.
The funnel docs owner maintains the guide inputs under docs/guides/. After updating either canonical page or its cited implementation, run node scripts/docs-build-funnel-guide.mjs, then make docs-build. Use node scripts/docs-build-funnel-guide.mjs --check to detect stale output. The generator uses the installed docs renderer, refreshes source contents/digests, and embeds the walkthroughs without external scripts, fonts or analytics. The HTML asset is docs/public/guides/funnel-recipe-parser.html.
Configure and previewOperator instructions
- Open Funnels in Flank and create a Character or General funnel. Your editor account needs
FUNNEL_EDITOR,ADMIN, orROOT;FUNNEL_EDITORis sufficient without granting general admin access. - For a Character funnel, choose RI only, VI only, or Both. RI means a real character; VI means a virtual character. Both provides a visitor choice between the two paths. General funnels skip recipe setup.
- For a Character funnel, select the recipe source for each character type. For a recipe family, choose which selectors are fixed and which the visitor answers.
- Rebuild Character steps from the selected sources. Rebuilding discards existing step edits, so finish source selection before customizing them.
- Edit supported copy and presentation, enable or disable optional native steps, arrange unlocked steps, and add custom interactions with Create custom step. Character funnels support recipe-free interactions before character selection and interactions inside a selected branch. General funnels have an ordered custom-step list. Review mobile/desktop and light/dark previews; previewing does not submit a visitor signup.
- Choose Explore or Character creation as the intended destination after signup, save the draft, and publish when the journey is ready. Arbitrary redirect URLs are no longer configurable.
Funnel names accept up to 200 Unicode code points, including non-ASCII letters and emoji. The create form, publishing checks and server enforce the same limit.
The builder derives the steps from recipe sources; it does not permit arbitrary changes to their recipe semantics. The public link serves the published snapshot, so saving draft edits alone does not update the visitor experience. Sources: builder, previews, and publication validation.
Build or reuse a custom stepOperator instructions
Choose Create custom step to open the library picker, then Use as base or Start from scratch. Choose Open Studio on an existing custom card to edit it. Describe any interaction, or use the input, wheel and landing starter ideas for inspiration. Input steps can collect a promo code or other generic value; code redemption, discounts and payments are outside this contract. A wheel inside a Character branch may use that branch's declared non-photo recipe choices. Studio receives those fields automatically from the branch; there is no separate attach control. General funnels and steps before character selection have no recipe fields. A landing can use finalized author images or videos and motion that respects reduced-motion preferences.
Use Components to choose which supported Foxy DS components the generator may import; the catalog lists all 33 public component groups with rendered examples and 11 selected by default. Search by name or description; filtering keeps your selections. The selected APIs, recipe fields, author media, per-instance settings and bounded conversation are sent to generation. Visitor photos are never included. Flank builds the result before presenting it. Fix preview error makes an explicit repair request; it does not change a published step by itself.
For the exact provider messages and DS update rules, see what goes to OpenRouter and DS integration. Adding a new DS component requires Studio registration; existing component prop changes enter future prompts after a DS rebuild and Flank deployment.
Settings contains layout controls, available recipe fields and the author-media upload list. Put configuration values, including JSON, directly in the prompt. Existing instance settings remain compatible. Upload JPEG, PNG, WebP, AVIF, MP4 or WebM files of at most 50 MiB each. Generation accepts up to 12 assets. Uploads are finalized into stable references before use, and previews receive fresh signed URLs. Reusing a shared library revision retains its finalized assets even when another editor created them.
Under Settings → Layout, Hide Back, Hide progress and Hide Skip hide those host controls for this step. When off, the funnel's navigation settings apply. Full screen removes the host title and content padding so the generated step fills the viewport. Skip stays visible unless Hide Skip is enabled. Failed steps still skip automatically. Layout defaults are off.
Use Edit text, choose a pencil beside visible text, then Apply or Reset. Changes appear immediately without a generation request. HTML and SVG text are editable; text baked into images, videos or canvas is not. Regeneration can change the text or its location, so an old edit applies only where both still match. Review the result after generating again. Source: text editor.
The library picker shows rendered previews of saved steps. Save to library stores a built step independently of the funnel draft. Studio autosaves every three minutes and shows its save status at the top right. Closing with unsaved changes offers Save and close, Discard and close, or Keep editing. Each Studio opening starts a fresh conversation; there is no saved-conversation browser.
Generating or repairing creates a durable job: status reads wait three seconds between responses, the whole job has a ten-minute deadline, and compilation repairs use at most three model attempts. A worker has a 90-second lease with 30-second renewal. A failed attempt preserves the last working result in the open conversation.
If a generation response or polling connection is lost, keep Studio open and use Reconnect generation to recover the same request. Reconnecting does not start another generation. Finish reconnecting before closing Studio to choose another step. Closing or refreshing ends this conversation; reopening does not recover its unfinished work. Wait for an upload to finish before closing Studio.
Choose a library revision to reuse it, or refine it and save a new revision. If another funnel references the item, Studio shows a notice and saving creates a separate library item. Otherwise, saving adds a revision to the same item. Deletion is blocked while any draft, published version or retained history references it. Add to funnel draft or Update draft step saves the library pin, settings, layout and text edits into the draft. Library saves include layout, settings and text edits as defaults for new uses. Existing instances keep their own values and pinned revisions. Each use has its own instance ID and local answers. Existing uses of an older revision remain pinned. Saving a library revision or adding it to a draft does not publish: review the draft and use Publish separately. Source: Studio UI, session persistence, generation jobs.
The builder card renders the actual pinned revision with this instance's settings and copy. Cards load near the viewport and do not submit answers or analytics. Click a card to edit it in Studio. Older saved artifacts support these previews and text edits without regeneration. Source: card preview.
Custom steps are optional for visitors. The component renders its primary action and calls complete; the host validates the values and advances. The host owns Back and Skip; Back follows the funnel's setting. If an artifact cannot load, become ready or execute successfully, the visitor continues past the step and the host records an error/skip. Native signup and recipe photo handling remain outside generated code. Preview each selected revision in its destination branch before publishing because declared recipe fields must be available in that branch.
RI strict modeOperator instructions
Strict mode is enabled by default and applies only to RI character creation. The Face & Chest photo step remains available. Other RI character settings and photo steps remain visible in the builder but are disabled for visitors and locked against enabling, editing and reordering. VI character steps, routing and signup remain available.
The yellow Strict mode — RI character creation notice appears for RI-only and Both funnels. Disable unlocks RI configuration; save and publish to apply that choice to the public journey. Strict mode preserves the saved step configuration and order rather than deleting them. The server also enforces RI edit locks and excludes restricted RI steps from the public definition. Sources: builder controls and server strict-mode rules.
Publication and archivalOperator instructions
| Action | Operator and visitor result |
|---|---|
| Save draft | Keeps edits for further work; the current publication stays unchanged. |
| Publish | Copies the draft to a new published version and makes the public link available. |
| Unpublish | Returns the funnel to draft status and disables its public link. |
| Archive | Keeps the funnel and its history, disables its public link, and makes the editor read-only. Use the Archived list filter to find it. |
| Reactivate | Returns an archived funnel to draft status. Review and publish again to restore the public link. |
Funnels are archived instead of permanently deleted. The legacy delete operation also archives. A missing, unpublished or archived public funnel falls back to the app home page, preserving its query parameters. Sources: lifecycle operations, public entry, and fallback policy.
Check and edit recipe sourcesOperator instructions
The funnel table shows recipe status only when the saved draft has an outdated or unavailable source, with an Edit source action. Current recipes, loading, missing sources and failed checks have no table chip; the editor summary shows all statuses. Recipe is up to date is green only when every source is current. Recipe has updates and Recipe unavailable are red; unavailable can mean deleted, unpublished or invalid. Loading, missing sources and failed checks use neutral labels. A failed check never appears as a current recipe.
Open Recipe details to inspect status and diagnostics, recheck sources, or choose Edit source. Source editing uses the existing explicit rebuild, which discards step customizations; it does not merge source changes into edited steps. There is no refresh preview, apply-refresh action or refresh undo. Updating a draft does not update the live publication. The public-funnel link remains in the table. Sources: editor summary, table, source details, and source rebuild.
Browse and restore published versionsOperator instructions
Use the Viewing dropdown at the top right of the editor to choose Draft or a published version. The separate Published version column, between Status and Recipe, shows the live publication number. Unpublished or archived funnels show the last publication marked Not live; new funnels show Not published. Unpublished edits do not receive a publication number. A published version fills the main editor in read-only mode: steps cannot be edited or reordered, and navigation and quick links are hidden. Browsing does not change the draft; Back to draft returns to your unsaved edits.
The bottom panel offers Restore version to draft. Restoring replaces the whole draft, including saved and unsaved unpublished edits, after confirmation. It keeps that version's recipe pins and destination rather than reparsing today's source. The current public version stays live until another publish.
For example, restore v1 while v3 is live, then publish: the new publication is v4. Versions 1–3 are unchanged. Archived funnels allow history browsing; reactivate before restoring, and publish separately to reopen the public link. Source: history service.
Find the PostHog eventsOperator instructions
Open Events from a funnel's table row. It uses the live publication when published; otherwise it shows the current draft. Draft filters omit the publication version until one is assigned. This is an event reference, not live activity, and opening it does not fire visitor events.
Copy dashboard instructions copies an LLM prompt with this definition's eligible steps, exact event filters and safe answer choices. Paste it into a PostHog-connected assistant to build a dashboard. It requests a vertical step-impressions column chart with every eligible step in journey order, unique-user counts and zero-count steps retained, plus an ordered funnel showing drop-off counts and percentages. Branch paths and versions stay separate. It also requests answer distributions and photo/skip counts. Drafts produce a template without an invented publication number; the assistant must verify the project, environment and available data. Source: dashboard instructions.
The dialog lists Step viewed, Step completed, and custom-step Step skipped, Step error and declared interactions, with copyable filters for each step. Titles are labels; they never become event names. Events carry funnel_id, string funnel_version and step_id. Recipe-branch steps also carry branch_id, so identical field IDs in different branches can be filtered separately. Entry and signup steps have no branch filter. Select the intended environment in PostHog.
| Event | When it fires | Additional properties |
|---|---|---|
funnel_step_viewed |
The visitor reaches the step. Local photo checking and email verification do not add views. | None. |
funnel_step_completed |
A valid answer or optional skip advances to the next step, including automatic advance after selecting an option. | answer for selected recipe option/boolean values; photo_count for photo steps; skipped: true for an unanswered optional step. |
funnel_step_skipped |
A custom step is skipped explicitly or after failure; this is not completion. | reason is visitor or runtime. |
funnel_step_error |
A custom artifact fails to load, become ready or execute. | Bounded error code, without raw exceptions or source. |
funnel_step_event |
A custom step sends an interaction declared by its manifest. | Declared interaction name only. |
Custom steps count as viewed only after the isolated frame is ready. Their completion uses an answers map containing only declared fixed-choice values; free text, promo codes and generic values without fixed choices stay in the browser. Unchanged custom completions are deduplicated, while separate uses of one library revision remain distinct through their instance IDs. Skips and errors do not imply successful completion. Keep optional custom steps out of required conversion milestones.
Answers retain declared recipe option values as primitives or arrays, including booleans. Object-shaped option payloads are omitted. Typed text, image content, filenames, URLs from uploads and signup credentials are not sent. Photo completion means the visitor continued past the photo step, not that a server upload or generation succeeded. Signup has a viewed event only; auth attempts and server conversions remain separate.
Visitors follow only their selected path and eligible conditional steps. Dropped and RI strict-mode-disabled steps are omitted. Views are deduplicated by branch and step within the mounted renderer; a reload can count again. Completing a native step again after going back emits another completion with the current answer. Invalid answers, restoring saved progress, going back and photo scanning do not emit completion events. Use unique users for drop-off comparisons rather than comparing raw event totals.
These events replace funnel_screen_viewed and its internal screen states. Existing PostHog reports using that event must switch to the new names. They do not prove signup completion or guarantee analytics delivery. Sources: view capture and step transitions.
Read the metricsOperator instructions
| Metric | Meaning |
|---|---|
| Impressions | Recorded public-funnel openings, not unique visitors. A new opening can count again; retries of the same opening event do not. |
| Conversions | Eligible new signups attributed to this funnel. A user is attributed once; repeated requests and later sign-ins do not add conversions or overwrite attribution. |
| Conversion rate | Conversions divided by recorded impressions. It does not measure payment or character completion. |
| Last impression | Server time of the most recently recorded new opening. |
Conversion requires a recorded impression and a verified account creation time at or after that visit, within seven days of it. Signing in to an account that existed before the visit is not a conversion. Attribution sets users.profile.funnel_id and increments the counter together. A valid signup from a previously recorded visit can still be attributed after the funnel is archived; archival prevents new impressions. Counters are per funnel, not per published version, and are not unique-visitor or character-creation counts. Sources: opening events, server signup attribution, and tracking transaction rules.
Visitor progress and current limitsOperator instructions
V2 retains the schema-2 browser record and adds optional customInputs[instanceId]. Generic interaction values remain scoped to that instance; accepted recipe writes still use the original stage/field keys. A General funnel stores no recipe snapshot or visitor photos. Signed author-media URLs, model prompts and source code are not visitor answers. Publication compatibility, seven-day expiry, account ownership and write-token checks still protect local progress. General navigation reaches the configured existing chooser or Explore even if local persistence fails; it does not depend on a character adapter.
The following Character-flow behavior preserves the existing native recipe and photo path. Visitors already signed in when they open a funnel go directly to Explore (/), before funnel data, local progress or opening events load. A visitor who starts anonymously and then signs up keeps the save-before-departure flow through /hop; that transition does not erase the saved character inputs.
Answers, selected photo files and the resolved recipe snapshot are retained in the visitor's browser using IndexedDB, with a seven-day expiry. This is local progress, not a pre-signup user or character record. The saved record includes RI/VI, gender when known, and original recipe stage/field keys. Changing the publication never moves old answers into a new recipe. On anonymous entry, unusable anonymous progress from an older publication, an expired record or a recognized older format is cleared automatically so the current funnel can start fresh. Valid progress resumes normally. Account-owned records, unknown future formats and records newer than the loaded publication are preserved. If progress cannot be restored, the funnel starts with empty inputs automatically, without a recovery screen. Background save failures do not add a banner; the next change retries saving. If the final signup save fails, signup continues without saved inputs. Character uses Explore; General keeps its configured destination. The main-layout quiz gate can still apply. /hop retains incompatible data and falls back to Explore. Sources: progress storage and recipe resolution.
Photo requirements come from the pinned recipe: accepted file types, byte size, minimum dimensions, pixel count and photo count. The browser reuses character-creation metadata and dimension checks. Missing or unsafe upload policies block selection instead of silently substituting guessed limits. HEIC/HEIF and AVIF are offered when the recipe accepts JPEG; WebP is converted only when the recipe excludes WebP but accepts JPEG. Brisket shares the Create flow’s browser converter, using the browser codec first and a dynamically loaded HEIC decoder as fallback. Conversion input has a 10 MiB safety ceiling; the converted JPEG must satisfy the recipe’s byte and dimension limits. WebP stays unchanged when the recipe allows it. No fixed 10 MB allowance overrides a stricter recipe limit.
A spinner covers the selected photo during local conversion and preview loading. Scanning starts only after the validated previews are decoded and visible. The success state and Continue appear together after the scan; preview failures allow another selection.
The funnel does not upload those photos to create a character, and its checking/accepted UI is not server-side identity or likeness approval. Signup waits for the latest browser save, then continues to /hop before the main-layout quiz gate runs. A failed save continues signup without saved inputs: Character uses Explore and General keeps its configured destination. The main-layout quiz gate can still apply. The saved recipe is not automatically applied to character creation. Use the separate character creation flow for the full creation process. Sources: photo checks, funnel state machine, and signup completion.
Signup attribution timingOperator instructions
Signup departure, account initialization and /hop show the same centered Foxy logo and spinner as the app loader. The normal transition has no intermediate status text or funnel skeleton; account initialization timeouts and transient failures silently continue to Explore; local progress failures continue without saved inputs. In /hop, unavailable authentication after ten seconds or an unexpected continuation error returns to foxy.ai without showing a recovery screen. Authenticated timeouts still continue to Explore.
Before leaving for signup, the server attempts to store the recorded visit in an
HTTP-only cookie. If preparation fails after its retries, departure reports a
best-effort funnel_signup_preparation_failed browser telemetry warning and
continues signup or sign-in. Saved inputs keep the /hop destination; failed
local saving uses Explore for Character or the configured destination for General. No departure retry screen is
shown, and reporting failure cannot block continuation. Cancellation still stops
the old session, and actual authentication errors remain visible. After authentication, server initialization attributes the app profile
before returning the app or authenticated API data. PostHog reads this saved
value; analytics availability does not determine whether attribution is written.
A failed attribution request can be retried without counting another conversion.
A timeout or transient failure at the initialization boundary logs a
funnel_initialization_failed warning to Axiom and clears the pending cookie
before redirecting to Explore. No retry screen is shown. The failed attempt
may leave conversion attribution unrecorded; it must not block account access.
Existing-account sign-ins and permanently ineligible visits clear the cookie
without preventing sign-in.
Selections and photos remain in the existing browser IndexedDB draft; this change
does not upload photos or start character creation automatically.
Sources: signup preparation and server attribution.
Ordinary signup and existing character creationOperator instructions
Until funnels launch, the main layout keeps the existing welcome redirect for signed-in users without a subscription or completed quiz. Ordinary and funnel signups, and returning sign-ins who have not finished onboarding, see one simplified quiz and its skippable paywall. skip-post-signup-quiz ON skips it; OFF, or an unavailable flag after the loading grace period, keeps it. Authentication routes remain unchanged. The gate preserves the current app path and query in a safe return_to, so paywall close or flag bypass resumes that destination. /hop resolves before this gate; its resulting funnel reference is retained in the return URL. Bypassing does not mark the quiz complete or consume saved inputs. See signup destinations for routing and removal points.
The app tour remains wired to its existing character-completion and first-purchase entry points through ?guide=true; signup and funnel continuation do not automatically start it. Existing character-creation rollout flags, payment checks, credits, KYC and the Explore RI/VI chooser remain in place. The chooser can still let someone change their mind; matching or discarding funnel prefills there belongs to the separate character adapter, not this release.
The browser record is claimed by the signed-in account before use. Other accounts cannot resume its contents. A stale tab cannot overwrite a newer save or delete it after transfer. The seven-day expiry advances on edits, not reads. The architecture reference describes the exact shape and reader API.
Deployment orderOperator instructions
Expose funnel links only after characters-v2 reaches 100% rollout. This release retains General's configurable Explore/Character creation destination. The launch condition is operational; it is not enforced by a new funnel flag check. Character-prefill transfer remains separate work. See the app handoff matrix.
For V2, apply 189_funnel_studio.sql after main’s migrations through 188, then deploy the matching Sirloin contract before Flank and Brisket. Studio requires FLANK_OPENROUTER_API_KEY on the Flank server; FLANK_STEP_STUDIO_MODEL optionally overrides the configured model. No browser credential is used. Drain older instances and reload editor/preview tabs before creating new pins or General funnels. Keep revision and retained-asset readers when rolling back behavior: existing publications must remain readable. See Step Studio architecture.
First remove legacy refresh controls from Flank before retiring their Sirloin endpoints. For the new funnel controls, deploy Sirloin before the matching Flank and Brisket releases, then reload old editor tabs before authoring or distributing links. New Flank clients call history and source-inspection RPCs that older Sirloin instances do not implement. Once a draft has a continuation destination, Sirloin rejects saves from older editors that omit it with “Reload the editor before saving this funnel.” Destination editing, version restoration and public funnel loading work without a rollout flag. The unused capability fields have been removed from API responses.
Drain older service instances and reload authoring and public-preview tabs before using funnels. Once new definition fields or schema-2 progress exist, rollbacks must retain readers and preservation for them. Do not revert to a Phase 1 binary, discard unknown JSON globally or rewrite immutable publications. See the compatibility rules.
Offline source browser
Snapshot reviewed 2026-10-21, relative to de714c72b705433be7789287250ac8b6fc427483. 3 files contain local changes. Each file has its own SHA-256 digest. A local snapshot is not evidence of a deployed or committed change.