Olio

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.

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 to live when 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_typeiphone / ipad / mac (multi-select)
    • app_version — semver min / max range
    • days_since_install — integer range
    • referral_id — from af_sub1 for influencer/affiliate flows (multi-select)
    • percentagemax: 0-100 for random rollout via stable hash
  • Schedule: optional startsAt / endsAt ISO8601 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 matched
  • X-Tryolio-Targeting-Rule: <id> — fell through to a targeting rule
  • (no X-Tryolio-Campaign or X-Tryolio-Targeting-Rule) — served the default

Common patterns#

Influencer / affiliate campaigns#

  1. In your MMP (e.g. AppsFlyer), generate a OneLink with a custom af_sub1 parameter:
    https://<your>.onelink.me/abcd?af_sub1=joelovesfitness
    
  2. Share the link with the influencer.
  3. In Olio, create a campaign with audience [{ type: "referral_id", values: ["joelovesfitness"] }] pointing at an influencer-specific variant.
  4. 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#

  1. Audience: [{ type: "media_source", values: ["facebook_ads"] }, { type: "percentage", max: 50 }]
  2. Variant A on this campaign; Variant B on a parallel campaign with max: 50 and lower priority — but for cleaner stats, flip a coin server-side via the percentage matcher.

Geo-specific copy#

  1. Audience: [{ type: "country", values: ["JP", "KR"] }]
  2. Variant assigns Japanese-localized copy on the welcome / paywall screens.

Day-1 vs day-7 onboarding#

  1. Two campaigns, both targeting app_version: { min: "2.0.0" }.
  2. Day-1 campaign: days_since_install: { min: 0, max: 0 }, priority 200.
  3. Day-7 campaign: days_since_install: { min: 7, max: 14 }, priority 100.
  4. 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_source dropdowns 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#

For deeper API reference see Tryolio/Dashboard/README.md (provisioning, token rotation) and Tryolio/OlioSDK/Sources/OlioSDK/Container/PersonalizableScreen.swift (slot vocabulary).