Docs · Integration guide

The integration guide

Guide v5 SDK 0.6.0 2026-10-13

This page is the canonical, hosted, versioned integration guide. The copy-paste block the dashboard generates for every new app references this URL, so when this guide improves, every future integration gets the improvement — nothing is baked into a stale prompt. If you were handed a prompt that points here: follow this page top to bottom, using the appID and apiKey from the prompt.

0.5.0 rename: the SDK was renamed from AppCoreKit (module) / AppCore.* (API) to FolioKit / FolioKit.*. New integrations should follow this page as written. Apps already integrated against the old names don't need to change anything: a legacy AppCoreKit product target and a deprecated AppCore typealias ship alongside FolioKit through the 0.x line, so existing code keeps compiling untouched. The old GitHub URL (github.com/Ryland990/AppCoreKit) redirects to the new one below.

What this SDK does

One SDK per app replaces PostHog + AppsFlyer: behavior events, screen tracking, session tracking, Apple Search Ads attribution, remote config, and RevenueCat revenue linkage, all flowing into the owner's unified dashboard. It is pure Swift, zero dependencies, iOS 16+, fire-and-forget (never blocks the main thread).

Step 1 — Add the package

Add the Swift package https://github.com/Ryland990/FolioKit, pinned to a version tag (from: "0.6.0") — never branch: main. 0.6.0 or later is required for in-app feedback (Step 6d); versions before 0.5.4 can upload the same batch twice and double app_opened. Target dependency: FolioKit. For XcodeGen projects, declare it in project.yml the same way.

Step 2 — Credentials

The app must be registered in the FolioKit dashboard (+ New app), which produces the appID (a short lowercase slug) and the apiKey. If you are an agent following a pasted integration prompt, the prompt already contains both — do not ask the owner for them. The API key is send-only: it can append events for this one app and read nothing, so it is safe to embed in the shipped binary.

Step 3 — Start the SDK

In the App struct's init (or application(_:didFinishLaunchingWithOptions:)), BEFORE RevenueCat is configured:

import FolioKit

FolioKit.start(appID: "<app-slug>", apiKey: "<api-key>")

Step 4 — RevenueCat handshake (CRITICAL)

The entire system links behavior to revenue through one shared user ID (the identity lifecycle explains why). Two paths — pick exactly one:

  • New app / fresh installs: configure RevenueCat with FolioKit's ID:
    Purchases.configure(with: .init(withAPIKey: "<rc key>")
        .with(appUserID: FolioKit.userID))
  • Existing app with live users: do NOT change RevenueCat IDs. Configure RevenueCat FIRST (unchanged), then start FolioKit adopting RevenueCat's existing identity:
    Purchases.configure(withAPIKey: "<rc key>")          // untouched
    FolioKit.start(appID: "<slug>", apiKey: "<key>",
                  userID: Purchases.shared.appUserID)     // adopt RC's ID
    RevenueCat entitlements are never affected; the behavior↔revenue join works for existing and new users alike.

If the app has no RevenueCat, skip this step entirely.

Owner task (once per app, ~2 minutes): add the RevenueCat webhook in the RevenueCat dashboard > Integrations > Webhooks > + New. The URL and the Authorization secret are shown in the app's settings (⚙) in the FolioKit dashboard, with copy buttons. Environment: both; events: all. This is the only manual step in the whole integration — an agent should surface it to the owner and move on.

Step 5 — Instrument screens

Attach .folioScreen("Name") to every top-level screen the user can spend time on. Choose names by reading the app's navigation structure:

  • Every tab root, every full-screen destination, the paywall, onboarding steps, and settings. Aim for 5–12 distinct screens.
  • Names are Title Case, human-readable, stable across releases ("Declutter", "Reader", "Paywall" — never SwiftUI type names).
  • Log each screen in exactly one place and under one name: .folioScreen on the view, or one manual FolioKit.screen("Name") call — never both. Mistakes seen in real apps: a tab logged both on tab selection and in the tab's view; a container and its first page logged at the same moment ("Onboarding" + "Welcome"); one screen under two spellings ("Daily Bites" / "DailyBites"). Each double-counts every visit.
DeclutterView().folioScreen("Declutter")

Step 6 — Instrument standard events

Use FolioKit.track(_:screen:props:) for moments that matter. REQUIRED standard events (exact names — the dashboard's funnel, the SKAdNetwork mapping, and the weekly brief all expect them):

EventWhen
onboarding_stepeach onboarding step, with props: ["step": n]
onboarding_completeduser finishes onboarding
paywall_shownpaywall becomes visible — once per presentation, always with props: ["trigger": "<what opened it>"] (e.g. onboarding, limit_reached, settings)
trial_starteda trial begins
purchase_startedthe user initiates a purchase, with props: ["plan": name]
purchase_completedpurchase confirmed by the store, with props: ["plan": name]
purchase_completed must fire from every path that can complete a purchase — not only the primary paywall. Audit the secondary paths explicitly: fallback paywalls (for example RevenueCatUI's presentPaywallIfNeeded purchaseCompleted: handler), exit-intent offers, and promo or offer-code redemption. A path that completes a purchase without firing the event stays invisible in funnels while RevenueCat still records the money, so the two dashboards silently disagree. Tag the source path in props (for example plan: revenuecat_fallback) so the dashboard can tell primary and fallback purchases apart. This rule comes from a real production gap.

(app_opened, session_end, screen_view, and ad_attribution are automatic — never track them manually.)

Then add 3–8 APP-SPECIFIC feature events for the app's core value loop, named verb_noun in snake_case (e.g. scan_started, scan_completed, book_finished, game_won). Instrument the moments a user gets value, not UI minutiae. When a feature has a clear success/failure, track both (scan_completed with props: ["deleted_count": n]).

Step 6b — Rating prompt & remote config

At the app's SUCCESS moments (scan finished, book completed, game won — pick 1–3 spots), add:

FolioKit.requestReviewIfAppropriate()

It's a no-op until enabled from the FolioKit dashboard (server-side config), self-limits by session count and cooldown, and never interrupts a first-time user. Do NOT build any custom review popup around it.

Optional, where the app has an obvious place for it: check FolioKit.updateRequired on launch and show the app's own "please update" UI when true (the minimum version is set server-side). Feature flags are available via FolioKit.flag("name") — don't invent flags unprompted.

Step 6c — Paid ads readiness (SKAdNetwork)

The SDK automatically reports install quality to ad networks via Apple's SKAdNetwork (conversion values mapped from the standard events; the mapping is server-side config — nothing to code). Two Info.plist additions make it work:

  1. SKAdNetworkItems: add the CURRENT official SKAdNetworkIdentifiers lists for the ad networks the owner runs or plans to run (e.g. Meta, TikTok, Reddit). Fetch each network's own published plist snippet from their developer docs — the lists change; do not hardcode from memory. Merge, dedupe, all lowercase, each ending in .skadnetwork.
  2. Track onboarding_completed, trial_started, and purchase_completed at the appropriate moments if not already instrumented (Step 6) — these drive the conversion values.

Do NOT add Meta/TikTok/Reddit SDKs; attribution flows through Apple. Do NOT add an ATT (App Tracking Transparency) prompt for this — SKAdNetwork needs no consent (the exact explainer).

Step 6d — In-app feedback (standard)

Every FolioKit app lets users write to the owner without leaving the app. The SDK has no UI: the app draws its own sheet in its own style (a message field and an optional email field) and owns when to offer it. Offer it from a "Send feedback" row in Settings, or as a dismissible prompt right after a success moment; never on launch, never mid-task, and not again after the user dismissed it.

let accepted = await FolioKit.sendFeedback(
    message: text,          // required, 1–4,000 characters after trimming
    email: replyEmail,      // optional; nil or "" means anonymous
    screen: "Settings",     // optional; defaults to the last screen reported
    props: ["rating": 4]    // optional extras (String / Bool / number)
)
  • Return value: true when the server accepted it or it was saved on the device to retry (offline, server error); show "Thanks!" either way. false when it was rejected and won't be retried: empty or over 4,000 characters, a malformed email, more than 5 from this user today, or a bad key. Enable Send only for a non-empty message and a plausible email, and disable it while the call is pending.
  • Where it goes: the owner's dashboard (Feedback tab), with a notification for each one. The text and email never go into analytics events; the SDK only tracks a feedback_sent count. Never track any part of the message yourself.
  • Privacy label: declare "Customer Support" and, if the sheet asks for one, "Email Address": linked to the user, not used for tracking, purpose App Functionality. Add the same to the app's PrivacyInfo.xcprivacy (NSPrivacyCollectedDataTypeCustomerSupport, NSPrivacyCollectedDataTypeEmailAddress) and tell the owner to declare both in App Store Connect App Privacy.

Step 7 — Verify (yourself — do not ask the owner)

Build, run in the simulator, tap through 3–4 screens, then background the app (events upload every ~15s and immediately on backgrounding). Then confirm the data arrived end-to-end with the verification endpoint, using the same credentials as FolioKit.start:

curl -s "https://shfpcykkzmzijpimpsrd.supabase.co/functions/v1/verify?minutes=15" \
  -H "x-app-id: <app-slug>" -H "x-api-key: <api-key>"

Success = "events_received": true with your tapped screens in event_names and an empty warnings array. If events_received is false, the response's hint says what to do; wait 30 seconds and retry once before debugging. warnings lists instrumentation mistakes the endpoint found in the window: screens logged twice or under two names, events fired in a loop, completions outnumbering starts, paywall_shown without a trigger, doubled app_opened, or an SDK older than 0.5.2. Fix every item, run the app again, and re-check until warnings is empty. When it checks out, tell the owner "you're live — events are flowing", and remove any test scaffolding.

Rules

  • Never block the main thread waiting on FolioKit; all calls are fire-and-forget, except sendFeedback, which is async (await it in a Task).
  • Never put PII (names, emails, photo contents) in event props. Free text from users goes through sendFeedback, never track.
  • Do not remove existing analytics unless the owner says so — running both in parallel during a trust-building period is normal and expected.
  • NEVER track app_opened, session_end, or ad_attribution manually — the SDK emits them automatically. Do not hook tracking to scenePhase changes.
  • Events must fire once per user action — never inside game loops, timers, per-tick/per-frame logic, or continuous publishers. Sanity check: paired events (x_started / x_completed) must have similar counts.
  • Expected volume is tens of events per session, not hundreds. If one session produces 500+ events, the instrumentation is wrong — fix it before finishing.

Version history

GuideSDKDateNotes
v50.6.02026-10-13In-app feedback becomes standard: FolioKit.sendFeedback(message:email:) from the app's own sheet, delivered to the owner's Feedback tab; privacy label adds Customer Support (+ Email Address).
v40.5.42026-09-24Requires SDK 0.5.4. Screens: .folioScreen, logged in exactly one place under one name. paywall_shown once per presentation with a trigger. The verify endpoint now returns warnings for instrumentation mistakes; success requires none (production lessons from three live apps).
v30.5.02026-07-27SDK renamed AppCoreKit/AppCore.* to FolioKit/FolioKit.* (package URL, imports, and all call sites). Legacy AppCoreKit product + deprecated AppCore typealias kept through 0.x, so pre-0.5.0 integrations keep compiling unchanged.
v20.4.02026-07-19Added purchase_started / purchase_completed to the required events and the every-purchase-path rule with source-path tagging (production lesson).
v10.4.02026-07-14First hosted version. Covers SPM install, one-ID RevenueCat handshake, screens, standard events, rating prompt & remote config, SKAdNetwork plist, self-verification.

FolioKit — an integration your agent performs and verifies, not a doc you decipher.

Start free