App Spec schema
The App Spec is the single source of truth for an app. A guided interview fills it in, you approve it, and every stage reads and writes it. "Plugging in your details" is filling this file.
It's plain YAML — portable, diff‑able, runtime‑agnostic. One file per app: app.appspec.yaml.
Top-level sections
meta— spec version + status in the pipeline (written by the producer).product— what the app is, for whom, how it earns (discovery).brand— name, bundle ID, icon, colors, App Store name (branding).architecture— screens, data, min iOS (architect).integrations— the provider chosen for each slot (architect).iap_products— subscription / purchase products (architect).aso— keywords, description, privacy labels (ASO).secrets_to_fill— the only values a human must provide.
product
product:
idea: string # one sentence
audience: string # specific, not "everyone"
core_loop: string # the repeated job-to-be-done
monetization: freemium # free | freemium | paid | ads
mvp_features: [string] # the cut list for v1
out_of_scope: [string] # explicitly deferred
brand
brand:
name: string # product name
bundle_id: string # reverse-DNS, e.g. com.example.exampleapp
accent_hex: "0xRRGGBB" # primary accent color
icon: path # generated 1024x1024 png
appstore_name: string # <= 30 chars, shown on the store
subtitle: string # <= 30 chars
architecture
architecture:
screens: [string]
data: SwiftData # SwiftData | none | CoreData
min_ios: "17.0"
uses_swift_charts: bool # extra frameworks the scaffold needs
integrations
Each value selects which adapter gets wired; none adds no SDK. The full provider list is in the integrations map.
integrations:
analytics: telemetrydeck # none | telemetrydeck | posthog | mixpanel | firebase
auth: none # none | apple | firebase | supabase
push: none # none | onesignal | fcm
iap: storekit2 # none | storekit2 | revenuecat
ads: none # none | admob
onboarding: launchkit # none | launchkit
iap_products
iap_products:
- id: string # must exist in App Store Connect
name: string
kind: auto_renewable # auto_renewable | non_renewing | non_consumable | consumable
price_usd: number
trial_days: number # 0 if none
best_value: bool # highlight in the paywall
aso
aso:
keywords: [string] # informs the 100-char keyword field
description: string # generated, human-approved
promotional_text: string
privacy_labels: auto # auto = derived from integrations; or list explicitly
category: string # App Store primary category
age_rating: "4+"
secrets_to_fill
The only things a human must supply. Names map to environment variables / a .env the Fastlane lanes and SDKs read. Never commit real values.
secrets_to_fill:
- APPLE_TEAM_ID
- ASC_KEY_ID # App Store Connect API key
- ASC_ISSUER_ID
- ASC_KEY_P8_PATH
- PRIVACY_POLICY_URL
# plus per-integration keys, e.g. TELEMETRYDECK_APP_ID, ONESIGNAL_APP_ID
Validation rules
The producer enforces these before a spec advances:
brand.bundle_idis reverse‑DNS and unique.brand.appstore_nameandbrand.subtitleare ≤ 30 characters.- Every
iap_products[].idis referenced by the paywall config and exists in App Store Connect before submission. - If
integrations.authuses a third party, Sign in with Apple is also required by App Review. aso.keywordsjoined length informs (doesn't exceed) the 100‑char keyword field.meta.statusonly advances through the gates in order:draft → idea_locked → brand_locked → building → pre_submit → submitted.