Core Concepts
The whole model, in one page. Read this once and the rest of the docs will click into place.
Project
A project is your workspace — one per app or brand. It owns your connected payment stores, your offering catalog, your funnels, your campaigns, your custom domain, and your analytics. Everything below lives inside a project.
Funnel
A funnel is a small React project. It’s the acquisition surface you build: a few pages, a flow between them, a paywall, and the config that ties them together. The canonical files:
funnel.ts # the spine: id, pages + order + routing, responses, offerings, checkout, locales
layout.tsx # persistent chrome around every page (header, progress bar)
styles.css # Tailwind entry + design tokens
pages/ # one component per page — filename matches the page key in funnel.ts
messages/ # optional per-locale translation catalogsYou edit those files. package.json is yours too — it’s where you add any npm packages your pages import (the Appfunnel SDK version is pinned for you). One file, mount.tsx, is generated from funnel.ts for you; leave it alone.
A funnel has two states:
- Draft — your live working copy. Every edit (in the editor, from the AI assistant, from a connected agent, or via
appfunnel push) autosaves to the draft. Nothing about the draft is served to real visitors. - Release — a locked snapshot of your funnel. Publishing turns your draft into a release. Releases don’t change after they’re made, so rolling back is just choosing an earlier one — nothing to rebuild, and visitors mid-flow finish on the version they started on.
Campaign
A campaign is what makes a funnel reachable. It has a URL slug and links one or more funnels. Your live URL is built from the campaign slug, not the funnel name.
Campaigns default to PAUSED. Only an Active campaign serves traffic — a paused campaign’s URL returns a 404. A newly created campaign serves nothing until you resume it.
Custom domain
Outside the preview link, a funnel only goes live on a custom domain you own and have verified. There’s no shared *.appfunnel.net address to fall back on, so attaching and verifying a domain is a required setup step before you launch. The upside: your funnels always live on your own brand.
Your live URL is always:
https://<custom-domain>/<campaign-slug>Optionally with a locale prefix and page: https://<custom-domain>/<locale>/<campaign-slug>/<page>.
Offerings vs funnel slots
This is the one distinction people trip on. There are two identities.
- An offering is a project-level catalog thing with a stable
key(likepro_monthly), a kind (subscription or one-time), a payment provider (Stripe, Paddle, Whop, SolidGate or Primer), and per-environment price bindings — a live price and a test price. It’s the funnel-facing merchandising handle; the price it binds is the charge identity, what actually gets billed. - A funnel slot is a local name your pages reference —
planin the template. Your paywall saysuseOffering('plan')and<Checkout offering="plan" />. Infunnel.ts, a slot maps to the offering key it sells:offerings: { plan: 'pro_monthly' }.
The slot is a stable seam. Your page code references plan and never changes; you swap what plan sells by reassigning it in the Offerings view. offerings: { plan: null } means the slot is declared but unassigned — useOffering('plan') returns undefined and the paywall shows a hint until you assign it.
Visitors and customers
A visitor starts out anonymous. When they enter their email, Appfunnel turns them into a known customer — and if that email already exists in your project, it’s the same customer, not a duplicate. That’s what ties an anonymous ad click to a paying customer across your analytics. Their quiz answers travel with them too, so you can segment your analytics by what people answered.
Releases
Publishing turns your draft into a release — a locked snapshot of your funnel — and points live traffic at it. A release never changes once it’s made, so what you published is exactly what visitors get. Rolling back is just choosing an earlier release: instant, reversible, and anyone mid-flow finishes on the version they started on.
The Releases popover in the editor is where you publish, roll back, and see your live status. The chip that opens it reads Live when your draft matches what’s published, and Draft when it doesn’t — either because you have unpublished changes or because you’ve never published.
Experiments
You can test a single page inside a funnel — the common case, and it leaves the rest of the flow alone — or a whole funnel against another. Either way you get up to four variants at once (your control plus three challengers), each reporting its own visitors, conversion rate, ARPU and revenue per paying customer, with the winner called on revenue per visitor. A variant is a whole page you author: pages/welcome@b.tsx is a variant of the welcome page, and the @ marks it. Appfunnel picks it up automatically. The wiring — which variants are in the test, their traffic split, the status, the winner — is managed for you from the Experiments dashboard, not in funnel.ts. Each visitor is held to one variant across visits, so returning traffic never sees the test flicker between versions. See Experiments.
Where agents fit
Three ways to reach the exact same draft:
- The AI assistant built into the editor edits your draft files from a chat prompt.
- A connected agent (Claude Code, Cursor, Codex) pairs to a funnel over MCP and reads, writes, and drives the preview through your open editor tab.
- The CLI pulls the funnel to your machine,
appfunnel devrenders it locally, andappfunnel pushsaves your draft back.
All three edit the draft. None of them publish — publishing is always yours to trigger. See Connected agents and the CLI.
Next
- Follow the end-to-end walkthrough in Getting Started.
- Start authoring in Build → Project structure.