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 and the API surface is AppCore.* — the original
working name, kept stable so shipped apps never need a rename. All code below is real.
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/AppCoreKit, pinned
to a version tag (from: "0.4.0") — never
branch: main. Target dependency: AppCoreKit. 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 AppCoreKit
AppCore.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 AppCore's ID:
Purchases.configure(with: .init(withAPIKey: "<rc key>") .with(appUserID: AppCore.userID)) - Existing app with live users: do NOT change RevenueCat IDs.
Configure RevenueCat FIRST (unchanged), then start AppCore 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 AppCore.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 .appCoreScreen("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().appCoreScreen("Declutter")
Step 6 — Instrument standard events
Use AppCore.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_completed | a purchase goes through |
(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:
AppCore.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
AppCore.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 AppCore.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
AppCore.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 AppCore; 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 |
|---|---|---|---|
| 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. |