Your funnels are real code

feature

Simon Steilgaard· Founder of Appfunnel

pages/paywall.tsxbuilt by clicking · owned as code

Appfunnel is the infrastructure layer for Web2App funnels. From today, every funnel you build is real code underneath: the same funnel.ts and page components whether you built them by clicking or by typing. Payments, analytics, attribution, and A/B testing stay handled; the difference is you can now open the repo.

Plug-and-play, until you open the repo

The builder is still the fastest way to ship a funnel. But some teams need pixel-perfect control: a design system to match, checkout logic the editor doesn't expose, or funnels that live in version control. You no longer have to choose: the funnel you build in the browser is real React you can pull down, extend, and push back.

What shipped

  • One spine, funnel.ts: id, the pages and their routing, the responses you capture, the products you sell, and the checkout provider, all in one typed file
  • Pages as components: every page is a .tsx file with your UI; SDK hooks handle navigation, responses, and checkout
  • The CLI: run pull, dev, push, and publish to work locally, preview against real data, and ship to our infrastructure
  • Everything still handled: Stripe checkout, session tracking, attribution, A/B testing, and RevenueCat sync work exactly as they do in the builder

The shape of it

Your whole funnel is one file, plus a component per page:

// funnel.ts
import { defineFunnel } from '@appfunnel-dev/sdk'
 
export const config = defineFunnel({
	id: 'funnel',
	checkout: 'stripe',
	responses: {
		email: { type: 'string', default: '' },
	},
	// Products are SLOTS: a local name your pages reference → a catalog product.
	products: { plan: null },
	pages: [
		{ key: 'welcome', type: 'default' },
		{ key: 'email', type: 'email-capture' },
		{ key: 'paywall', type: 'paywall' },
		{ key: 'finish', type: 'finish' },
	],
})
// pages/paywall.tsx
import { definePage, useProduct, Checkout } from '@appfunnel-dev/sdk'
 
export default definePage(function Paywall() {
	const product = useProduct('plan')
	if (!product) return null
 
	return (
		<section>
			<h1>{product.name}</h1>
			<p>{product.price.formatted}</p>
			<Checkout product="plan" surface="element" />
		</section>
	)
})

Work locally, then publish in one step:

appfunnel pull my-funnel
appfunnel dev
appfunnel push && appfunnel publish

What this means for Appfunnel

The funnel you build point-and-click and the funnel you write in code are the same artifact, so you can start plug-and-play and graduate to full control without migrating anything. The builder isn't going anywhere; it just stopped being a ceiling.