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_id is reverse‑DNS and unique.
  • brand.appstore_name and brand.subtitle are ≤ 30 characters.
  • Every iap_products[].id is referenced by the paywall config and exists in App Store Connect before submission.
  • If integrations.auth uses a third party, Sign in with Apple is also required by App Review.
  • aso.keywords joined length informs (doesn't exceed) the 100‑char keyword field.
  • meta.status only advances through the gates in order: draft → idea_locked → brand_locked → building → pre_submit → submitted.