Most mobile attribution platforms are black boxes. You install an SDK, it phones home to the vendor's servers, and a number shows up in a dashboard. You have no way of knowing how that number was calculated, what data was collected to produce it, or where that data went afterward.
The bigger problem is what happens before the SDK even initializes. Traditional MMPs sit between your app and third-party ad platforms Facebook, TikTok, Google Ads, the whole ecosystem. When a user clicks an ad on one of those platforms and later installs your app, the MMP tries to connect those two events. The word "tries" is doing heavy lifting in that sentence. They collect dozens of device signals from both sides IP, user agent, screen resolution, timezone, device model, installed fonts — and run a probabilistic matching algorithm that essentially guesses whether the person who clicked the ad is the same person who installed the app.
The matching algorithm is proprietary. You can't see the code. You can't verify the logic. You just trust the number.
We built Grovs because we think attribution should work differently. The entire stack is open source under MIT — the backend, the dashboard, the SDKs for iOS, Android, React Native, and Flutter. You can read every line of code that touches your users' data. Self-host the whole thing if you want. Build the SDKs from source if you don't trust our CDN.
This post explains how attribution actually works in our codebase — the redirect flow, the matching logic, the difference between Android and iOS, and where we draw the line.
The fundamental issue with traditional MMPs isn't that they're expensive or slow. It's that they're middlemen.
When you use AppsFlyer, Branch, or Adjust, you're inserting a third party into the relationship between your app and your ad spend. The MMP receives click data from ad networks (Facebook, TikTok, Google), receives install data from your app's SDK, and tries to connect the two using probabilistic matching. The MMP needs to see data from both sides — and from as many apps as possible — because the matching model gets better with more cross-app data. That's why these platforms build device graphs and shared fingerprint databases. More data, better guesses.
This creates three problems:
Your data feeds their model. Every install, every event, every purchase your app sends to the MMP is used to improve their matching across their entire network. Your data is their product.
Probabilistic matching degrades. Apple killed IDFA with App Tracking Transparency. Google is tightening the Play Advertising ID. Every OS update removes a signal that probabilistic models depended on. The MMPs compensate by collecting more ambient data — which is exactly what regulators are trying to stop.
You can't verify the numbers. The matching algorithm is a black box. When the dashboard says "1,247 installs attributed to your TikTok campaign," you have no way of knowing whether that's 1,247 verified connections or 800 real ones padded with 447 statistical guesses. They look the same in the dashboard.
We don't sit between your app and ad networks. We don't receive click data from Facebook or TikTok. We don't build device graphs.
Instead, Grovs gives you links. You use those links in your campaigns, your social posts, your emails, wherever. When someone clicks a Grovs link and installs your app, we connect the click to the install using first-party data — no third-party signals involved.
Here's how it works on each platform, because the mechanism is genuinely different between Android and iOS, and we think it's dishonest to pretend otherwise.
Every Grovs project gets custom domains. When a user clicks one of your links, it resolves through our link engine. The engine checks the user's platform and does the right thing:
App is installed: The link opens the app directly via Universal Links (iOS) or App Links (Android). The SDK receives the link payload — campaign data, custom parameters, whatever you attached when you created the link. Attribution is instant and deterministic. The app opened from your link. Done.
App is not installed: The link redirects to the App Store or Google Play. This is where deferred deep linking kicks in, and where Android and iOS diverge.
On Android, attribution is clean. When the link redirects to Google Play, the referrer data is passed through Google's Install Referrer API. Google Play stores this data and makes it available to the app after install. When your app opens for the first time, our SDK extracts the referrer, sends it to the backend, and we attribute the install to the original link.
This is fully deterministic. Google Play cryptographically ties the referrer to the install package. There's no guessing involved. The user clicked your link, Google Play recorded it, and the app retrieved it on first open.
iOS doesn't have a referrer mechanism like Android. Apple doesn't pass data through the App Store install flow. So for deferred deep links on iOS, we use IP-based matching.
Here's the honest version of how it works:
User clicks your link on their iPhone. Our backend logs the click as an Action record — binding the device to the link — and caches the device fingerprint in Redis. The fingerprint includes the IP address, user agent, and screen dimensions.
The user goes to the App Store, installs the app, and opens it. The SDK calls our backend with the device's current IP and user agent.
The backend checks Redis for a cached fingerprint from the same IP within the last 5 minutes. If it finds one, it compares the user agent (platform, version). If multiple devices match on IP and user agent (say, two iPhones on the same WiFi), it falls back to screen dimensions, timezone, and WebGL data to break the tie.
If exactly one device matches, attribution succeeds. The SDK receives the link payload and tracking data. If no match or ambiguous match, we don't attribute.
The matching code lives in fingerprinting_service.rb in the backend repo. You can read the exact logic: IP lookup, user agent comparison, collision resolution. Nothing hidden.
Is this probabilistic? In a strict sense, yes — we're making an educated guess that the device that clicked the link 3 minutes ago from the same IP is the same device that just opened the app. But there's a meaningful difference between this and what traditional MMPs do:
We match within a 5-minute window, not hours or days. If the user doesn't install within 5 minutes of clicking, we don't attribute. This dramatically reduces false positives.
We match on exact IP + exact user agent, not on statistical similarity across dozens of fuzzy signals.
We don't build device graphs or cross-reference data from other apps. The matching happens per-project, isolated.
We don't fall back to weaker signals when the strong ones don't match. No match means no attribution.
The code is open source. You can read exactly how the matching works, audit the 5-minute window, verify that we're not doing anything beyond what we describe.
Traditional MMPs use IP as one of dozens of signals fed into a proprietary algorithm that produces a confidence score. We use IP as a simple lookup key within a narrow time window. The difference matters.
We didn't just open-source the SDKs. The full stack is public.
Backend — Rails 8.1, PostgreSQL 16, Redis, Sidekiq
This is the attribution engine, link resolver, and analytics aggregator. Multi-tenant: each account is an Instance with test and production Projects. Subdomain routing separates concerns:
sdk.* — where mobile SDKs authenticate, post events, resolve deferred deep links
go.* — link redirects, no auth, this is the quick links engine
api.* — dashboard API, analytics queries, configuration
preview.* — link preview cards for social sharing
The attribution-relevant files:
File | What it does |
|---|---|
| Redis caching of click fingerprints, IP-based matching, collision resolution |
| Device lookup by vendor ID, fingerprint matching orchestration |
| Resolves deferred deep links — finds matching Action, returns link payload |
| Click handler — creates Action record, caches fingerprint |
| Action lookup with 5-minute TTL enforcement |
Five Sidekiq processes handle async work: event ingestion, batch processing, scheduling, device updates, and maintenance.
Dashboard — Next.js 15, React 19, TypeScript, Tailwind, shadcn/ui
Campaign performance, link metrics, revenue tracking, user segmentation. Same codebase whether hosted or self-hosted.
iOS SDK — Swift, iOS 13+
Universal Links, deferred deep linking, link generation, in-app messaging, push, StoreKit 2 revenue tracking. When the app opens, the SDK calls data_for_device on the backend. If there's a matching Action, the backend returns the link payload through the delegate:
func grovsReceivedPayloadFromDeeplink(
link: String?,
payload: [String: Any]?,
tracking: [String: Any]?
)
That tracking dictionary carries the attribution data — campaign, source, medium, whatever you attached to the link.
Android SDK — Kotlin, API 21+
App Links, Google Play Install Referrer, DeeplinkDetails objects with link, payload, and tracking data. Both callback and Kotlin Flow patterns.
React Native and Flutter — wrappers around the native SDKs.
The backend repo includes Docker Compose and Kamal configs. Clone, copy .env.example, generate encryption keys, docker compose up. PostgreSQL, Redis, Rails, five Sidekiq workers, all containerized. App runs on port 8765.
The SDKs accept a baseURL parameter:
Grovs.configure(APIKey: "your-key", baseURL: "your-domain.com")
Your data never touches our servers. The SDKs append API paths automatically — just point them at your domain.
The ee/ directory in the backend contains enterprise features — in-app purchase tracking and revenue analytics with Apple and Google webhook handlers. This requires a Grovs subscription for production use. The core attribution engine, link system, analytics, messaging, and all SDKs are MIT with no restrictions.
We went with a dual license because we need revenue to keep building, and revenue tracking is the feature enterprises value most. The alternative was gating everything behind a paid license, which would defeat the point. This felt like the honest trade-off.
A closed-source MMP benefits from collecting as much data as possible. More data feeds their probabilistic models. More apps in their network improve their device graphs. Your data makes their product better for everyone including your competitors.
An open-source, first-party attribution platform has no incentive to collect more than what's needed for the match. We don't build device graphs. We don't cross-reference data across projects. The matching logic doesn't depend on volume. It depends on the link.
That's the structural difference. Not "we promise not to sell your data" but "our architecture doesn't benefit from having your data in the first place."
The code is at github.com/grovs-io. Star it, fork it, self-host it, audit it. We built it in the open because we think attribution should work that way.
More articles you might find useful