Skip to the content.

Appmiral platform — full deep dive (114 apps)

Phase 2 of the comparison. Phase 1 (01-on-device-cohort.md) compared 11 festival apps installed on a single device. This phase extends the analysis to the entire Appmiral festival-app stack scraped from https://appmiral.com/our-work, then closes the loop with a runtime probe of the 7 CrowdConnected-club apps that could be sideloaded onto a real device (Pixel 10a, Android 16, 23 Jun 2026).

🚨 Headline finding (runtime, 23 June 2026)

The TT Festival app was caught actively exfiltrating location data to AWS in the US during this analysis. With the app freshly launched on a phone sitting in NL (distance to Assen ~150 km), the CrowdConnected SDK:

  1. Pulled valid settings from mobile.crowdconnected.com/prod/settings/dd54d39d/... — the static-analysis-only active=false from the earlier June check has flipped to active=true.
  2. Initialised the geofence observer with surface UUID e52e1cbe-0260-4469-a68c-b0570da3f779.
  3. Started the outer-zone observer at 900 s poll interval (15 min) — then immediately transitioned to “high settings” with a 30 s poll interval.
  4. Signed Sigv4 requests to firehose.us-east-1.amazonaws.com and called Firehose_20150804.PutRecordBatch carrying 15 964 bytes of buffered positions — the first batch timed out, the SDK retried.
  5. Also signed Sigv4 requests to sns.eu-west-1.amazonaws.com (push registration).

The raw evidence is in data/ttfestival_runtime_logcat_2026-06-23.log (502 KB, adb logcat -d -v threadtime, captured 23 Jun 2026 ~22:07 UTC). Key lines:

GEO_OBSERVER: Setting Zones
GEO_OBSERVER: Setting geo surface: e52e1cbe-0260-4469-a68c-b0570da3f779
GEO_OBSERVER: Started geo observer with frequency: 900 seconds
GEO_OBSERVER: Transitioning to high settings
GEO_OBSERVER: Started geo observer with frequency: 30 seconds
KINESIS : Sending batch of positions via kinesis
KINESIS : 15964 Bytes stored on disk
AWS4Signer: host:firehose.us-east-1.amazonaws.com
AWS4Signer: x-amz-target:Firehose_20150804.PutRecordBatch
AWS4Signer: 20260623/us-east-1/firehose/aws4_request

This is the first evidence that the TT app’s CrowdConnected pipeline is no longer dormant. It also directly contradicts the privacy policy’s claim that “all personal data is stored in EU (eu-west-1), with Cloudflare as the only exception” — the SDK is actively signing requests for firehose.us-east-1.amazonaws.com (Virginia, US).

Runtime state per CrowdConnected-club app (11 of 12 probed)

Update — 24 June 2026. All remaining installable CC-club apps were sideloaded (full XAPK bundles pulled via a Cloudflare-bypassing Playwright/Firefox downloader, then adb install-multiple with the config splits) and re-probed. 11 of the 12 CC-club apps are now installed and probed — only com.appmiral.werchterparklife remains uninstallable (APKPure only archives a 32-bit armeabi_v7a variant, which the arm64-only Pixel 10a rejects with INSTALL_FAILED_NO_MATCHING_ABIS). Two more apps were caught with an active geo observer running, and two others returned an explicit server active=false “stopping modules” — confirming the kill-switch is live and server-controlled across the fleet. Raw logs: data/runtime_logs2/<pkg>.log, summarised in data/cc_runtime_state_round2.csv.

App CC SDK init? Settings poll result Live tracking state Kinesis push?
com.appmiral.ttfestival YES valid active=true (23 Jun) 🔴 LIVE — 30 s polling, sending to Firehose us-east-1 (23 Jun; quiet in 24 Jun 35 s probe) YES (15.9 KB on disk)
be.appstrakt.werchter2010 YES observer restored 🟠 ACTIVE-LOW — geo observer running at 1800 s (outer/30-min cadence) attempted (0 B buffered)
com.appmiral.dutchgp YES observer restored 🟠 ACTIVE-LOW — geo observer running at 1800 s (was dormant 23 Jun → observer restarted 24 Jun) no
com.appmiral.mdlbeast YES active=false → stopping modules dormant — consent-enabled, server kill-switch OFF no (0 B)
com.appmiral.rockforpeople YES active=false → stopping modules dormant — consent-enabled, server kill-switch OFF no (0 B)
com.appmiral.sicknewworld YES Settings was null dormant (indoor-positioning config; crowdConnectedIndoorLocation=true) no
com.appmiral.loversandfriends no n/a SDK present, not enabled within 35 s probe (consent not passed) no
com.appmiral.museumnachtamsterdam no n/a SDK present, not enabled within 35 s probe (consent not passed) no
com.appmiral.teamnl no n/a SDK present, not enabled within 35 s probe (consent not passed) no
com.appmiral.tomorrowlandbrasil no n/a SDK present, not enabled within 35 s probe (consent not passed) no
com.appmiral.werchterboutique no n/a SDK present, not enabled within 35 s probe (consent not passed) no
com.appmiral.werchterparklife — — not installable (32-bit-only APKPure archive) —

Across every app that did initialise the SDK — werchter2010, dutchgp, mdlbeast, rockforpeople, sicknewworld, plus TT — the AWS Cognito identity pool is the same US pool us-east-1_xsD5yIcIS seen in the TT capture (e.g. CognitoIdentityProviderDeviceCache.us-east-1_xsD5yIcIS.* in the dutchgp/mdlbeast logs). The push-registration leg signs sns.eu-west-1 (EU), but the location/identity pipeline is wired to us-east-1 fleet-wide. The only thing separating “dormant” from “live 30 s exfiltration” is the server-side active boolean — which we observed in all four states within 24 h: active=true + high-frequency (TT, 23 Jun), active=true + outer-zone observer running (werchter2010, dutchgp, 24 Jun), explicit active=false → stopping modules (mdlbeast, rockforpeople), and Settings was null (sicknewworld).

The “no SDK init” rows mean the app starts the AWS Cognito caching machinery (visible in logs as CognitoCachingCredentialsProvider) but never calls CrowdConnected.enable() because consent has not been granted by tapping through the onboarding flow. This is not the same as “dormant” — it’s “unknown until the user opts in”. The CC SDK and libpositioner.so are still bundled and would activate the moment consent is given (subject to the server-side active flag at that time).

For the two apps where the SDK did initialise (TT Festival + Dutch GP), the contrast is stark:

The two apps share identical Cognito infrastructure (User Pool us-east-1_xsD5yIcIS, app client 31iqmf255m1dai4bpqq3an55pg), identical Kinesis Firehose target (firehose.us-east-1.amazonaws.com), and identical SDK code. The only difference is a single boolean in the settings JSON returned by mobile.crowdconnected.com/prod/. That boolean is server-side and can flip at any moment, for any of the 12 CC-club apps, without a new app release.

Methodology — runtime probe

Scope and method

Raw artifacts live under ~/.copilot/session-state/<id>/files/appmiral-deep-dive/: apks/, base_apks/, manifests/, badging/, dex/, privacy/, plus the matrices perm_matrix.csv, sdk_matrix.csv, privacy_claims.csv.

TL;DR

Cohort

Group Count
Total listed on appmiral.com/our-work ~212
Appmiral-stack packages 139
APKs retrieved 114
Privacy policies retrieved 100
Apps with CrowdConnected DEX 12
Apps with ACCESS_BACKGROUND_LOCATION 24
Apps with BLUETOOTH_SCAN 12
Apps with FOREGROUND_SERVICE_LOCATION 11

The “CrowdConnected club” (12 apps, 10.5 %)

All 12 apps bundle net.crowdconnected.* classes, ship the AWS Kinesis Firehose SDK and request the full surveillance permission set. The cohort cuts across countries, genres and operators — it is not a regional or one-promoter pattern.

Package Version Country / festival Promoter
com.appmiral.ttfestival 1.2.0¹ NL — TT Festival Stichting TT Week Assen
com.appmiral.dutchgp 5.2.0 NL — Dutch GP (F1) Circuit Zandvoort
com.appmiral.museumnachtamsterdam 4.0.0 NL — Museumnacht Stichting Museumnacht
com.appmiral.teamnl 4.0.3 NL — TeamNL Huis NOC*NSF
com.appmiral.werchterboutique 7.0.2 BE — Werchter Boutique Live Nation BE
com.appmiral.werchterparklife 1.0.2 BE — Werchter Parklife Live Nation BE
be.appstrakt.werchter2010 16.1.0 BE — Rock Werchter (legacy pkg) Live Nation BE
com.appmiral.tomorrowlandbrasil 3.0.0 BR — Tomorrowland Brasil We The Fest
com.appmiral.rockforpeople 4.0.1 CZ — Rock for People Ameba Production
com.appmiral.mdlbeast 6.0.2 SA — MDLBEAST Soundstorm MDLBEAST
com.appmiral.loversandfriends 1.0.0 US — Lovers & Friends (Las Vegas) Goldenvoice
com.appmiral.sicknewworld 2.0.0 US — Sick New World (Las Vegas) Goldenvoice

¹ This is the APKPure-mirrored version; the on-device build pulled from a phone running TT Festival 2026 was v2.0.1 (versionCode=5). The same tracking stack is present in both.

Identical permission shape

Eleven of twelve declare exactly:

ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
ACCESS_BACKGROUND_LOCATION
BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_SCAN, BLUETOOTH_CONNECT
ACCESS_WIFI_STATE, CHANGE_WIFI_STATE
FOREGROUND_SERVICE, FOREGROUND_SERVICE_LOCATION

com.appmiral.loversandfriends substitutes ACTIVITY_RECOGNITION for FOREGROUND_SERVICE_LOCATION — same SDK, different runtime mode (likely a coarser proximity-tracking variant for the Vegas venues).

Identical service declaration

Every member of the club registers a foreground service backed by Crowd Connected:

<service android:name="net.crowdconnected.androidcolocator.service.CrowdConnectedService"
         android:foregroundServiceType="location"
         android:exported="false" />

This is the smoking-gun: the service is white-labelled by Appmiral and dropped into every CC-enabled build. It is identical down to the attribute order.

Per-app CrowdConnected configuration (from each APK’s strings.xml)

Each Appmiral build ships per-festival Crowd Connected credentials as Android string resources, plus a boolean that toggles the indoor positioning system (Bluetooth beacon scanning + native libpositioner.so):

Package Festival CC appKey Indoor (BLE) active? Bundled libpositioner.so?
com.appmiral.ttfestival TT Festival 0e0957e1 (apk mirror) / dd54d39d (on-device v2.0.1) false — IPS dormant yes (shipped, not started)
com.appmiral.dutchgp Dutch GP 36fe15a0 false — IPS dormant yes
com.appmiral.museumnachtamsterdam Museumnacht Amsterdam 30d143df false — IPS dormant yes
com.appmiral.teamnl TeamNL Huis 48b6d281 false — IPS dormant yes
com.appmiral.werchterboutique Werchter Boutique 8d9ea3c7 false — IPS dormant yes
com.appmiral.werchterparklife Werchter Parklife 142f2a03 false — IPS dormant yes
be.appstrakt.werchter2010 Rock Werchter (legacy) 490f94a3 false — IPS dormant yes
com.appmiral.tomorrowlandbrasil Tomorrowland Brasil 9ccdd0a0 false — IPS dormant yes
com.appmiral.rockforpeople Rock for People 4ddafa25 false — IPS dormant yes
com.appmiral.mdlbeast MDLBEAST d61997e4 false — IPS dormant yes
com.appmiral.loversandfriends Lovers & Friends (Las Vegas) 34c90857 false — IPS dormant yes
com.appmiral.sicknewworld Sick New World (Las Vegas) 34500b15 true — IPS WIRED yes

crowdConnectedIndoorLocation is the Appmiral-wrapper flag that decides whether CrowdConnected.enable() adds the IPSModule (Bluetooth beacon ranging) on top of the always-present GeoModule (outdoor GPS) and BackgroundModule. Setting it to "true" means the build is provisioned to scan for Eddystone beacons inside the venue; setting it to "false" ships the SDK with the BLE/positioner pipeline dormant — the binary is there but never started.

Eleven of twelve apps ship the SDK with BLE positioning dormant. Only Sick New World (Las Vegas) has the IPS module wired in. This is consistent with Lovers & Friends declaring ACTIVITY_RECOGNITION rather than FOREGROUND_SERVICE_LOCATION in its manifest — those two Las Vegas Goldenvoice events share the same venue and appear to be the only two apps where outdoor + indoor crowd analytics are both intended at runtime.

Where the inner/outer radius lives — and why it can’t be read from the APK alone

For TT Festival the on-device runtime measurement showed an outer radius of 60 km around Assen with an inner high-frequency zone of ~820 m. Those numbers are not hardcoded in the APK. They come from a JSON settings/<appKey> response that the SDK fetches from https://mobile.crowdconnected.com/prod/ on launch. The response also carries the boolean active flag, the list of deployed Eddystone beacons, the venue’s surface (floor map) and any high-frequency geo-zones.

Crucially, that endpoint is gated by AWS Cognito User Pool authentication (pool us-east-1_xsD5yIcIS, app client 31iqmf255m1dai4bpqq3an55pg) and a SigV4-signed call against execute-api. The user-pool client is configured with a client secret that is not present in plain text inside the APK strings or DEX — it is either constructed at runtime in the SDK code or stored in a way that requires the SDK to run for it to be assembled. As a result:

To establish live state for any of the other eleven apps would require either:

  1. installing the app on a phone, accepting consent, and capturing the settings/<appKey> HTTPS response with a TLS-intercepting proxy (mitmproxy + bypassing the SDK’s pinning, where present); or
  2. attaching a Frida hook to CrowdConnected.enable(...) or SettingsPollWorker to dump the in-memory settings object after it is deserialized.

Both are runtime techniques that require physical access to the app per festival. A static-only analysis can show that the SDK is wired, the permissions are declared and the indoor flag is set — it cannot prove that data is currently flowing.

The “background-location only” cohort (12 apps)

These apps declare ACCESS_BACKGROUND_LOCATION but do not bundle CrowdConnected. They ship the Google Play Services location library (com.google.android.gms.location) which is the standard primitive for geofences and the Fused Location Provider. The most plausible use is geofence-triggered local notifications (e.g. “you arrived at the festival” or “you parked here”), not crowd analytics. The fact that BLUETOOTH_SCAN and FOREGROUND_SERVICE_LOCATION are missing rules out the CC surveillance pattern.

Package Version Festival
com.appmiral.afaslive 1.0.5 AFAS Live (venue)
com.appmiral.alcatrazfestival 2.2.0 Alcatraz Festival
com.appmiral.beatvenues 1.4.1 be·at Venues
com.appmiral.bittersweet 2.0.0 BitterSweet Festival
com.appmiral.brusselsphilharmonic 3.0.3 Brussels Philharmonic
com.appmiral.dmugen 5.0.3 DIF DM-ugen
com.appmiral.glucksgefuhle 4.0.4 Glücksgefühle Festival
com.appmiral.jellingmusikfestival 6.0.2 Jelling Musikfestival
com.appmiral.sunsetbeachfestival 2.0.2 Sunset Beach Festival
com.appmiral.turf 2.0.2 TURF
com.appmiral.zwartecross 9.0.2 Zwarte Cross
com.triple.verstappen 1.1.0 Max Verstappen Official

Platform-wide SDK matrix (114 apps)

SDK Bundled in % Notes
Firebase Analytics 113 99 % Universal; only one missing app is appmiral.freaksofnature
Firebase Messaging 113 99 % Push delivery
Firebase Crashlytics 113 99 % Crash reporting
Google Maps 112 98 % Programme maps, venue maps
OkHttp + Retrofit 113 99 % HTTP stack
AWS Cognito 111 97 % Appmiral backend identity
Google Play Billing * — Where in-app purchases exist
AWS Kinesis Firehose 12 10 % Exclusively in the CC club
CrowdConnected 12 10 % See above
Cookie Information CMP 99 87 % The Appmiral default consent manager
OneTrust CMP 32 28 % Used in parallel/instead of Cookie Information on some builds
Permutive 30 26 % Audience analytics
Jetpack Compose 22 19 % Modern UI
Adjust 0 0 % —
AppsFlyer 0 0 % —
Branch 0 0 % —
Mixpanel 0 0 % —
Amplitude 0 0 % —
Segment 0 0 % —
Airship 0 0 % —
Braze 1 <1 % Named in 43 policies
Insider 0 0 % Named in 41 policies
Salesforce MC 1 <1 % Named in 41 policies
Tealium 2 <2 % Named in 41 policies
OneSignal 1 <1 % —
Sentry 1 <1 % —
Bluedot / Radar / Gimbal / Estimote 0 0 % No “other” beacon vendors

What the cohort tells us

The Appmiral platform is remarkably homogeneous on the engineering side (Firebase + AWS + OkHttp/Retrofit + Maps in 97–99 % of apps) and very narrow on optional add-ons. There is essentially one crowd-tracking vendor on the platform — CrowdConnected — and either Cookie Information or OneTrust as the consent manager. The TT Festival build is unusual only in shipping the com.appmiral.privacy.none module (no consent manager at all); 99 of 114 builds ship Cookie Information.

Privacy policy reality check

Per the 100 policies retrieved (cross-correlated with each app’s actual code):

Boilerplate over-disclosure (the “fake processor list”)

The shared Appmiral template names a long list of optional processors. None of the named third parties — Braze, Insider, Salesforce Marketing Cloud, Tealium — are actually integrated in more than a single build. Concretely:

Vendor Policies that name them Builds that actually bundle them
Braze 43 1
Tealium 41 2
Insider 41 0
Salesforce Marketing Cloud 41 1
CrowdConnected 41 12

This is what the boilerplate looks like in practice. For example com.appmiral.dranouter (a Belgian rock festival with no Bluetooth or background-location code) names CrowdConnected, Braze, Insider and Salesforce in its privacy policy. The policy describes processors that don’t exist in the build.

That is the opposite sin from TT Festival’s. TT Festival’s policy fails by under-disclosing (no US transfer, no 60 km, no account-id linkage); Dranouter and ~38 peers fail by over-disclosing (naming a phantom processor stack as if it were in use).

Bundle-but-silent (the dangerous gap)

Of the 12 apps that actually ship CrowdConnected:

Package Policy mentions CC? Policy discloses US transfer? Policy claims EU-only?
com.appmiral.ttfestival yes no yes (false)
com.appmiral.dutchgp yes no yes (false)
com.appmiral.sicknewworld no yes no
com.appmiral.rockforpeople no no no
com.appmiral.werchterboutique no no no
com.appmiral.werchterparklife no no no
com.appmiral.museumnachtamsterdam no no no
com.appmiral.tomorrowlandbrasil no no no
com.appmiral.loversandfriends no no no
be.appstrakt.werchter2010 (no policy hosted) – –
com.appmiral.mdlbeast (no policy hosted) – –
com.appmiral.teamnl (no policy hosted) – –

Two of twelve mention CrowdConnected at all. Both of those two claim EU-only storage despite the CC stack streaming to AWS us-east-1. One discloses any US data transfer, and that one is silent on CC itself. Three operators don’t expose a discoverable privacy policy at all for the app variants that ship the tracker.

60 km / radius disclosure

Zero policies mention the 60 km collection radius. This is the figure measured on-device in the TT Festival app (docs/crowd-connected.md). It is plausibly a platform-wide default; we have no evidence that any festival has narrowed it, nor that any has disclosed it.

Firebase Analytics — the universal silent default

113 of 114 apps bundle Firebase Analytics. 42 of 100 policies mention “Firebase” by name. The remaining 58 policies are silent about Firebase Analytics specifically, typically referring to “third-party analytics” generically or omitting it entirely.

Where TT Festival sits

Against the Appmiral platform as a whole TT Festival is unusual in three very narrow respects:

  1. The shipping build replaces com.appmiral.privacy.cookieinformation with the stub com.appmiral.privacy.none. 99 / 114 peer apps ship a real consent manager.
  2. The privacy policy is more honest than 10 of its 11 CC-club peers: it names CrowdConnected by name and links to CC’s own policy. Two of twelve CC-club members do this — TT Festival and Dutch GP.
  3. The published “Safety” and “FAQ” pages on the festival’s own website are completely silent about location tracking, where some peers at least carry the information in the in-app policy.

On every other axis — the 60 km radius, the AWS us-east-1 data destination, the account-id linkage via sendAlias, the SOS-framed background-location prompt, the hardcoded credentials in resources — TT Festival is a faithful clone of the Appmiral CC template. If anything the company-level findings apply more strongly to the rest of the club, where the tracker is shipped with no disclosure at all.

Implications

Next steps

Update (Phase 3 — 03-ondevice-egress-dutchgp-museumnacht.md): two of these gaps are now closed. Tapping through onboarding inside the geozone, Dutch GP was caught performing a live collect+upload of a precise GPS position to firehose.us-east-1.amazonaws.com (submitAllRecords sent 2 records, keyed to deviceId 382c3ab8…). Museumnacht turned out to be already registered+consented (correcting “consent not passed”), but with the server runtime gate activeAPIResponse=…,false,… it collected nothing during the window. The legacy apps’ real kill-switch is that server value, not the local geoEnabled flag.