Configuration
Every headless funnel has two configuration layers: a project-level config file (appfunnel.config.ts) that declares variables, products, and settings, and per-page exports that define routing and metadata. Both use type-safe helper functions provided by the SDK.
defineConfig
The defineConfig function is used in appfunnel.config.ts at the root of your project. It provides full type checking and autocompletion for all configuration options.
import { defineConfig } from '@appfunnel-dev/sdk'
export default defineConfig({
projectId: 'proj_abc123',
name: 'My Funnel',
responses: {
goal: { type: 'string' },
experience: { type: 'string' },
interests: { type: 'stringArray' },
},
data: {
showDiscount: { type: 'boolean', default: false },
},
queryParams: {
utm_source: { type: 'string' },
ref: { type: 'string' },
},
products: {
items: [
{ id: 'monthly', name: 'Monthly', storePriceId: 'price_xxx' },
{ id: 'yearly', name: 'Yearly', storePriceId: 'price_yyy', trialDays: 7 },
],
defaultId: 'yearly',
},
})Config properties
type AppFunnelConfig = {
// Required
projectId: string // Project identifier from the dashboard
name: string // Funnel display name shown in the dashboard
// Required
initialPageKey: string // Page key of the first page (e.g. 'welcome'). Filename without .tsx.
// Optional
funnelId?: string // Auto-set after first publish — do not set manually
defaultLocale?: string // Default i18n locale code (e.g. "en")
// Variables
responses?: Record<string, VariableConfig> // answers.* variables (user responses)
data?: Record<string, VariableConfig> // data.* variables (internal state)
queryParams?: Record<string, VariableConfig> // query.* variables (from URL params)
// Payments & integrations
products?: ProductsConfig
integrations?: Record<string, Record<string, unknown>>
settings?: FunnelSettings
}FunnelSettings
type FunnelSettings = {
disableToasts?: boolean // Disable SDK's automatic error toasts (default: false)
}VariableConfig
Each variable declaration specifies a type and optional defaults.
type VariableConfig = {
type: 'string' | 'number' | 'boolean' | 'stringArray'
default?: any // Initial value
persist?: boolean // Whether to persist across sessions
}All variables must be declared here. The three variable namespaces — responses, data, and queryParams — map to answers.*, data.*, and query.* prefixes in hooks like useVariable. Variables that are not declared in this config will be ignored at runtime — hooks like useResponse, useData, and useQueryParam will not read or write undeclared keys. The only exception is the five default UTM query parameters (utm_source, utm_medium, utm_campaign, utm_content, utm_term), which are included automatically.
ProductsConfig
type ProductsConfig = {
items: ProductConfig[] // Array of product definitions
defaultId?: string // Product ID selected by default
}
type ProductConfig = {
id: string // Unique identifier within the funnel
name: string // Display name
storePriceId: string // Price ID from your payment provider (e.g. Stripe price_*)
trialDays?: number // Free trial days before the first charge
}definePage
Each file in src/pages/ exports a page constant created with definePage. This sets the page metadata and routing rules.
Page files must be directly in src/pages/ — nested folders are not supported. File names should be lowercase with dashes (e.g. choose-goal.tsx, checkout.tsx). The file name (without extension) is the page key, used in routes, initialPageKey, and URL paths. index.tsx is not supported — use a descriptive name instead (e.g. intro.tsx, welcome.tsx).
import { definePage } from '@appfunnel-dev/sdk'
export const page = definePage({
name: 'Choose Your Goal',
type: 'default',
routes: [
{ to: 'next-page' },
],
})
export default function ChooseYourGoal() {
return <div>Page content here</div>
}Page properties
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name shown in the dashboard and analytics. |
type | PageType | No | Categorizes the page for analytics grouping. |
slug | string | No | URL slug. Defaults to the filename without the extension. |
routes | RouteConfig[] | No | Ordered list of navigation routes. The first matching route is used. |
PageType
The type field accepts one of the following values:
'default' | 'checkout' | 'finish' | 'auth' | 'paywall' | 'upsell'
Setting the correct type allows the dashboard to group pages in analytics views. For example, pages with type 'paywall' appear in conversion funnels automatically.
RouteConfig
Routes are evaluated in order. The first route whose when condition is met (or has no condition) determines where useNavigation().next() sends the user.
| Property | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Target page slug (the filename without the extension, e.g. 'checkout' for checkout.tsx). |
when | ConditionConfig | ConditionGroupConfig | No | Condition that must be satisfied for this route to activate. If omitted, the route always matches. |
ConditionConfig
A single condition that checks one variable against one operator.
type ConditionConfig = {
variable: string // Variable to check (e.g. 'answers.goal', 'device.isMobile')
equals?: VariableValue // Loose equality (==). For arrays, compares length.
notEquals?: VariableValue // Loose inequality (!=). For arrays, compares length.
contains?: string // String contains substring
greaterThan?: number // Numeric > comparison. For arrays, compares length.
lessThan?: number // Numeric < comparison. For arrays, compares length.
exists?: boolean // Value is defined, not null, and not empty string
isEmpty?: boolean // Empty array, blank string, or falsy value
includes?: string // Array includes this value
}ConditionGroupConfig
Combine multiple conditions with AND/OR logic. Groups can be nested.
type ConditionGroupConfig = {
operator: 'AND' | 'OR'
rules: (ConditionConfig | ConditionGroupConfig)[]
}Conditional routing examples
Routes are evaluated top to bottom. Place specific conditions first and a catch-all route last.
export const page = definePage({
name: 'Experience Level',
routes: [
{ to: 'advanced-tips', when: { variable: 'answers.experience', equals: 'advanced' } },
{ to: 'beginner-guide' }, // default fallback
],
})Multiple conditions with AND/OR:
export const page = definePage({
name: 'Pricing',
routes: [
{
to: 'premium-checkout',
when: {
operator: 'AND',
rules: [
{ variable: 'answers.budget', greaterThan: 50 },
{ variable: 'answers.interests', greaterThan: 2 }, // array length > 2
],
},
},
{ to: 'basic-checkout' },
],
})Check array membership:
{ to: 'yoga-plan', when: { variable: 'answers.interests', includes: 'yoga' } }Always include a fallback route (one without a when condition) as the last entry. If no route matches, navigation will not proceed.