Olio in 5 minutes
Olio personalizes iOS onboarding without app updates. The iOS dev wraps screens in a PersonalizableScreen; marketers author variants and launch campaigns from a web dashboard; the SDK fetches the right variant per user at runtime.
This guide takes you from zero to a live targeted campaign. Five sections, each ~5 minutes.
1. Provision a tenant#
A tenant is your isolated namespace in Olio. Variants, campaigns, and screen contracts all live under it.
# Generate a strong write token (any string works; a hex secret is conventional)
TOKEN=$(openssl rand -hex 24)
# Provision the tenant by writing its dashboard token to KV
cd Tryolio/Backend
npx wrangler kv key put "dashboard_token:<your-tenant>" "$TOKEN" \
--namespace-id=00a075eb3ac0491c91ace348c6c37af1
Replace <your-tenant> with a slug like acme or wisprflow. The token is the credential you'll paste into the dashboard sign-in screen.
Tenants without a dashboard_token exist in read-only mode (the iOS SDK can fetch variants if any exist, but no one can author from the dashboard).
2. Sign in to the dashboard#
Open https://tryolio-dashboard.vercel.app — Vercel SSO will gate access to your team. Once in, you'll see the Olio sign-in form.
- Tenant: the slug you just provisioned
- Dashboard token: the secret from Step 1
- (Advanced: custom Worker URL — leave empty for production)
After sign-in you land on the Campaigns page. The left sidebar shows your tenant context, primary nav (Campaigns / Variants / Screens / Targeting / Integrations / Metrics), and a 4-step setup checklist.
The token is stored in your browser's localStorage and never sent to Vercel or anywhere outside your machine.
3. Define your screens (the iOS contract)#
Before authoring variants, the iOS team needs to declare which screens are personalizable and what slots each one has. This is the screen schema — a per-screen JSON document declaring the ordered layout of static iOS chrome and dynamic Olio slots.
Option A: Generate from iOS code (recommended)#
If your iOS app already uses PersonalizableScreen, run the Claude skill:
In a Claude Code session pointed at your iOS project:
/olio-export-schema
The skill walks your Swift sources, finds PersonalizableScreen(id: "...") calls, classifies the surrounding view tree (static elements vs dynamic slots), and emits one screen_schema.json per screen — plus an upload command you can run.
Option B: Hand-author via the dashboard#
Navigate to Screens in the sidebar. For each screen you want to personalize, add elements:
- Dynamic slot:
{ type: "dynamic", slot: "hero" }— references a slot key your iOS code declares - Static placeholder:
{ type: "static", label: "App logo · 60pt · centered" }— describes iOS-side chrome that surrounds the slots
Order matters — schemas describe top-to-bottom rendering order in the Preview pane.
Why bother#
Without a schema, the dashboard's Preview tab composites slots in arbitrary dict order and shows nothing about the iOS chrome surrounding them. With a schema, marketers see realistic previews and the SDK can warn (in dev builds) when a variant references a slot the schema doesn't declare.
4. Integrate the iOS SDK#
Add the package to your iOS project (Package.swift or Xcode UI):
.package(url: "https://github.com/your-org/tryolio-sdk", from: "1.0.0")
In your app entry point:
import OlioSDK
@main
struct YourApp: App {
init() {
// Configure once at app launch.
// Olio auto-detects MMP SDKs (AppsFlyer, Adjust) and forwards
// attribution; auto-collects device_type / app_version /
// days_since_install. No additional iOS-side wiring needed.
Task {
let resolver = NetworkVariantResolver(
configuration: .init(
baseURL: URL(string: "https://tryolio-variants.dev-sto.workers.dev/<your-tenant>")!
)
)
await Olio.shared.configure(resolver: resolver)
}
}
var body: some Scene { WindowGroup { ContentView() } }
}
Wrap personalizable screens:
struct WelcomeScreen: View {
var body: some View {
PersonalizableScreen(id: "welcome") {
VStack(spacing: 32) {
MediaSlot(id: "hero") {
HeroIcon(systemName: "leaf.fill") // ← default content
}
HeadingSlot(id: "heading") {
Text("Find your moment of calm") // ← default content
}
CTAGroupSlot(id: "cta_group") {
Button("Get started") { ... } // ← default content
}
}
}
}
}
The default closures are what users see when (a) no variant matches, (b) the variant load fails, or (c) the network is down. Olio is fail-open — your app never crashes from a bad variant.
5. Author a variant + launch a campaign#
Author a variant#
Navigate to Variants in the sidebar → + New variant. The filename is <screen>.<variantKey>.json — e.g. welcome.fb_sleep.json.
Use the Form tab to author with typed inputs (HeadingContent, CTAContent, MediaContent, CTAGroupContent) or the JSON tab for power-user editing. The Preview tab renders the variant inside a phone-frame mock with your screen schema's static placeholders composited in.
Save. The variant is now in KV and reachable at:
GET https://tryolio-variants.dev-sto.workers.dev/<tenant>/welcome.fb_sleep.json
Launch a campaign#
Navigate to Campaigns → + New campaign. Configure:
- Name + status: status starts as
draft; flip tolivewhen ready - Priority: higher wins on ties (e.g. seasonal campaigns at 1000, evergreen at 100)
- Audience: matchers (any combination, all must pass for the campaign to fire):
country— Cf-IPCountry header (multi-select)media_source— from MMP (multi-select)device_type—iphone/ipad/mac(multi-select)app_version— semvermin/maxrangedays_since_install— integer rangereferral_id— fromaf_sub1for influencer/affiliate flows (multi-select)percentage—max: 0-100for random rollout via stable hash
- Schedule: optional
startsAt/endsAtISO8601 bounds - Variants: per-screen variant assignment (one variant key per screen)
Save and flip status to live. Resolution priority on every request:
1. Live campaigns sorted by priority (first match wins)
2. Targeting rules (legacy — for advanced use)
3. Default variant ('<screen>.json' if it exists)
Verify with curl:
curl "https://tryolio-variants.dev-sto.workers.dev/<tenant>/welcome.json?id=test_user&ctx_media_source=organic" \
| jq .variantId
The response headers tell you which path resolved:
X-Tryolio-Campaign: <id>— a campaign matchedX-Tryolio-Targeting-Rule: <id>— fell through to a targeting rule- (no
X-Tryolio-CampaignorX-Tryolio-Targeting-Rule) — served the default
Common patterns#
Influencer / affiliate campaigns#
- In your MMP (e.g. AppsFlyer), generate a OneLink with a custom
af_sub1parameter:https://<your>.onelink.me/abcd?af_sub1=joelovesfitness - Share the link with the influencer.
- In Olio, create a campaign with audience
[{ type: "referral_id", values: ["joelovesfitness"] }]pointing at an influencer-specific variant. - Users who install via Joe's link see Joe's variant; users from Sarah see Sarah's; everyone else sees the default.
A/B test on a paid channel#
- Audience:
[{ type: "media_source", values: ["facebook_ads"] }, { type: "percentage", max: 50 }] - Variant A on this campaign; Variant B on a parallel campaign with
max: 50and lower priority — but for cleaner stats, flip a coin server-side via the percentage matcher.
Geo-specific copy#
- Audience:
[{ type: "country", values: ["JP", "KR"] }] - Variant assigns Japanese-localized copy on the welcome / paywall screens.
Day-1 vs day-7 onboarding#
- Two campaigns, both targeting
app_version: { min: "2.0.0" }. - Day-1 campaign:
days_since_install: { min: 0, max: 0 }, priority 200. - Day-7 campaign:
days_since_install: { min: 7, max: 14 }, priority 100. - Different welcome / encouragement variants per cohort.
What Olio does NOT do (yet)#
- Build influencer link infrastructure — we lean on your MMP's OneLink / tracker URLs. Future option for tenants without an MMP.
- Handle iOS Universal Links / deferred deep linking — your MMP does this.
- Live MMP API integration — currently the dashboard hydrates
media_sourcedropdowns from a CSV upload (download AppsFlyer's installs report, drop into Integrations page). Live REST API integration is on the roadmap. - Per-campaign analytics — request counts and conversion tracking are coming. Today the dashboard surfaces no metrics; the Worker emits
X-Tryolio-Campaign: <id>response headers that you can pipe into your existing analytics. - Audit log — change history is on the roadmap.
Reference#
- Worker: https://tryolio-variants.dev-sto.workers.dev —
Tryolio/Backend/src/worker.ts - Dashboard: https://tryolio-dashboard.vercel.app —
Tryolio/Dashboard/ - iOS SDK:
Tryolio/OlioSDK/(Swift Package) - Demo iOS app:
Tryolio/OlioDemo/ - Claude skills:
/olio— generate variant payloads from campaign briefs/olio-export-schema— generate screen schemas from Swift sources
For deeper API reference see Tryolio/Dashboard/README.md (provisioning, token rotation) and Tryolio/OlioSDK/Sources/OlioSDK/Container/PersonalizableScreen.swift (slot vocabulary).