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.
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.5.0") — never
branch: main. 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:
RevenueCat entitlements are never affected; the behavior↔revenue join works for existing and new users alike.Purchases.configure(withAPIKey: "<rc key>") // untouched FolioKit.start(appID: "<slug>", apiKey: "<key>", userID: Purchases.shared.appUserID) // adopt RC's ID
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 .folioKitScreen("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).
DeclutterView().folioKitScreen("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):
| Event | When |
|---|---|
onboarding_step | each onboarding step, with props: ["step": n] |
onboarding_completed | user finishes onboarding |
paywall_shown | paywall becomes visible (any trigger) |
trial_started | a trial begins |
purchase_started | the user initiates a purchase, with props: ["plan": name] |
purchase_completed | purchase 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:
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.- Track
onboarding_completed,trial_started, andpurchase_completedat 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 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. If false, the response's hint says what to
do; wait 30 seconds and retry once before debugging. Sanity-check the counts
against the rules below (paired x_started/x_completed
events must have similar counts). 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.
- Never put PII (names, emails, photo contents) in event props.
- 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, orad_attributionmanually — the SDK emits them automatically. Do not hook tracking toscenePhasechanges. - 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
| Guide | SDK | Date | Notes |
|---|---|---|---|
| v3 | 0.5.0 | 2026-07-27 | SDK renamed AppCoreKit/AppCore.* → 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. |
| v2 | 0.4.0 | 2026-07-19 | Added purchase_started / purchase_completed to the required events and the every-purchase-path rule with source-path tagging (production lesson). |
| v1 | 0.4.0 | 2026-07-14 | First hosted version. Covers SPM install, one-ID RevenueCat handshake, screens, standard events, rating prompt & remote config, SKAdNetwork plist, self-verification. |