BehaviorGuard’s day-one risk score uses a four-classifier ensemble (z-score, Mahalanobis distance, kernel density estimation, and a small Core ML model) tuned against a synthetic baseline population. That works. It catches gross anomalies; it produces a score band most fraud teams can act on; it’s a real signal from the moment the SDK is integrated.
What it doesn’t do, by design, is model YOUR app’s user population. The baseline assumes a generic distribution of touch pressures, scroll velocities, typing rhythms, and motion patterns. Your app’s actual users have a tighter distribution: people who use a children’s-banking app have a different motor profile than people who use a private-banking app, who in turn have a different profile than people who use a wallet for crypto traders. A per-app model trained on your population is materially more accurate than the generic baseline.
The privacy question: how do you train such a model without ever shipping a single biometric event off the device? BehaviorGuard’s answer is on-device training via Core ML’s MLUpdateTask, with the trained model staying on the user’s device. Here’s how that works.
The two-stage training pipeline
Stage 1 happens at app install: a baseline model ships in your app bundle as BehavioralBaseline.mlmodel. It is the result of BehaviorGuard’s internal training against synthetic and permission-gathered population data. Every user starts with this model.
Stage 2 happens after the user has been using your app for ~7 days (configurable). The framework has accumulated ~50,000 to 200,000 biometric events in the on-device ringbuffer. The framework spins up an MLUpdateTask:
import CoreML
import BehaviorGuardSDK
let baseURL = Bundle.main.url(
forResource: "BehavioralBaseline",
withExtension: "mlmodelc"
)!
let baselineModel = try MLModel(contentsOf: baseURL)
let trainingData = try BehavioralEventCollection
.recentSamples(
since: Date().addingTimeInterval(-7 * 24 * 60 * 60),
limit: 200_000
)
.map { event in event.asMLFeatureProvider() }
let trainingProvider = try MLArrayBatchProvider(array: trainingData)
let updateTask = try MLUpdateTask(
forModelAt: baseURL,
trainingData: trainingProvider,
configuration: MLModelConfiguration(),
completionHandler: { context in
// The updated model is in context.model.
let updatedURL = ApplicationSupport.appendingPathComponent(
"BehavioralPerApp.mlmodelc"
)
try? context.model.write(to: updatedURL)
let detector = CoreMLAnomalyDetector()
try? detector.loadModel(at: updatedURL)
}
)
updateTask.resume()
The trained model is written to the app’s Application Support directory, then loaded into a CoreMLAnomalyDetector (the Core ML member of BehaviorGuard’s four-classifier ensemble); subsequent risk-scoring cycles use the per-app model instead of abstaining.
The model is never uploaded anywhere. The training samples are never uploaded anywhere. Everything happens inside the app’s sandbox.
The privacy properties this gives you
-
No biometric data leaves the device. GDPR Article 9 (special-category data) is satisfied because there is no processor: the data is captured, used, and discarded on the same device.
-
No model leaves the device. The per-app trained model is the result of computation on biometric data; under some interpretations of GDPR and BIPA, the model itself is biometric data. Keeping it on-device closes this question.
-
The user can purge. BehaviorGuard’s
purgeUserData()method deletes the trained model along with the on-device biometric ringbuffer. The user’s next session starts again with the baseline. -
The trained model is per-user, not per-app. This is a subtle but important detail: the model is trained on THIS specific user’s behavior, so it’s effectively a biometric template. If the device is shared (rare on iOS but possible), the model captures the patterns of whoever used the device most. BehaviorGuard’s
MultiUserMode(off by default) splits the training data per-user identity, training distinct models. Enable this if your app actually has multiple users per device.
What you give up
On-device training is genuinely harder than cloud-based training. Three real trade-offs:
Trade-off 1: Smaller model + simpler features
MLUpdateTask supports a limited set of model types: nearest-neighbor classifiers (with k from 1 to 21), small fully-connected neural networks (up to ~2 MB), and the Pipeline-style composite models that wrap these. You can’t train a Transformer on the device. You can’t train a CNN with millions of parameters. The model BehaviorGuard’s baseline ships is a tightly-tuned 8-layer feedforward network of about 1.2 MB; on-device fine-tuning operates on the last 2 layers (the “head”), keeping the earlier feature-extraction layers frozen.
The trade-off: a fully cloud-trained model could be richer. The privacy gain is worth it for most regulated apps.
Trade-off 2: Less data than cloud-trained models
A cloud-trained model can be trained on biometric events from millions of users. The on-device model sees data from one device. For some classes of patterns (rare attack motifs, novel collusion strategies, the harder cases in the BehaviorGuard threat model), this matters. BehaviorGuard mitigates by combining the on-device model’s output with the baseline model’s output, blending the two via a weighted average. The blend weight is configurable; the default starts at 80% baseline / 20% on-device after the first training, shifting to 50/50 after the second training cycle (typically 30 days in).
Trade-off 3: Compute cost + battery
Training a model on the device costs compute. MLUpdateTask uses the Apple Neural Engine when available (A12+) and the GPU as fallback. On an A15+ device with 200,000 events, the training takes 8 to 15 seconds. BehaviorGuard schedules training only when the device is plugged in AND screen-locked, using BGTaskScheduler for the trigger. The user experiences zero impact.
The PSI drift detector and when to retrain
The Population Stability Index (PSI) is the framework’s signal for “your model is becoming stale.” PSI measures the divergence between the distribution of current biometric events and the distribution the current model was trained on. When PSI exceeds a configurable threshold (default 0.25), the framework triggers a retraining cycle.
Common causes for high PSI:
- User’s life changed: new job, different commute pattern, switched from right-hand to left-hand use after an injury. Genuine drift; retraining catches it.
- iOS update changed a sensor’s calibration: the accelerometer’s bias coefficient changed in an iOS minor release. Spurious drift; retraining handles it but it’s worth a metric to detect “drift hit 10 of my users on the same day” (which is the leading indicator).
- Device hardware change: user got a new phone. The Secure-Enclave key invalidates and the framework resets the model to baseline.
The retraining schedule is automatic. The framework also exposes a manual triggerRetraining() method for cases where you want to force it (post-iOS-update, post-app-major-version).
Threat models the on-device model handles better
A per-app trained model improves on the baseline for:
-
Detecting account takeover. The compromised attacker has the credentials but doesn’t have the muscle memory. The per-user model is calibrated to one person’s biometrics; the attacker is, by definition, anomalous against it. Account takeover scores ~20% higher (more risk-signaled) on the per-user model.
-
Detecting motor-impaired attackers. If the legitimate user has, e.g., a specific tremor pattern, that pattern shows up in the per-user model. An attacker without the same condition triggers anomaly even if their typing is otherwise similar.
-
Detecting fast-fingers spoofing. Some attackers attempt to replay a captured biometric pattern at unrealistic speed. The per-user model has a tighter speed distribution and catches this.
What it doesn’t help with:
- Cold-start. During the first 7 days before the model trains, you’re on the baseline. New-account fraud is the harder problem; baseline plus heuristic rules (velocity-based, geolocation-based) is what’s actually doing the work in that window.
What BehaviorGuard ships
The framework provides:
- Default baseline model with 4 classifiers (Stage 1).
- On-device training pipeline (Stage 2) via
MLUpdateTask. - PSI drift detector with automatic retraining triggers.
purgeUserData()for explicit forgetting.- Per-user mode for shared-device deployments.
- Blend-weight configuration for tuning baseline-vs-trained mix.
- Audit-log entries for every training event (timestamp, sample count, training duration, resulting model size). No biometric data in the log, just metadata.
Summary
A behavioral-biometrics model that ships with synthetic-baseline-only training works on day one but doesn’t differentiate your app from a vendor’s reference implementation. On-device training adds the per-app accuracy lift without trading away the privacy posture you bought the SDK for. The cost is modest: a 15-second training cycle, scheduled when the device is plugged in and locked. The output is a model that’s calibrated specifically to your app’s users, that never leaves their device, and that purges with a single call when they ask it to.
Engineering opinion, not advice. This post reflects the author's engineering reasoning at time of publication. It is not professional security, legal, financial, or compliance advice; do not rely on it as a substitute for review by qualified professionals for your specific situation. Posts may become outdated as iOS, Apple frameworks, attacker techniques, and our own SDKs evolve. The canonical source of truth for shipped SDK behavior is /docs and /changelog.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.