This guide covers how to build a full parental control application using Phosra's mobile SDKs. It walks through the complete lifecycle from family setup to on-device enforcement, covering both iOS and Android.
A Phosra-powered parental control system has four components:
+---------------------+ +------------------+
| Parent Dashboard | | Phosra API |
| (Web or Mobile) | ------> | (REST + Webhooks)|
+---------------------+ +--------+---------+
|
+----------------+----------------+
| |
+--------v---------+ +---------v--------+
| iOS SDK | | Android SDK |
| (FamilyControls | | (UsageStats, |
| ManagedSettings| | VPN, DeviceAdmin|
| DeviceActivity)| | Accessibility) |
+------------------+ +------------------+
| |
+--------v---------+ +---------v--------+
| On-Device | | On-Device |
| Enforcement | | Enforcement |
+------------------+ +------------------+
Data flow:
The parent creates a family and adds a child via the Phosra API or TypeScript SDK:
const setup = await phosra.setup.quick({
child_name: 'Emma',
birth_date: '2016-03-15',
strictness: 'recommended',
});
// setup.family.id, setup.child.id, setup.policy.idFrom the parent's app, register the child's device. This returns a one-time API key that the child's device stores locally.
import PhosraSDK
let config = PhosraConfiguration(
parentToken: parentJWT,
childID: childUUID
)
let client = PhosraAPIClient(configuration: config)
let request = RegisterDeviceRequest(
deviceName: "Emma's iPad",
deviceModel: UIDevice.current.model,
osVersion: UIDevice.current.systemVersion,
appVersion: "1.0.0",
capabilities: ["FamilyControls", "ManagedSettings", "DeviceActivity"]
)
let response = try await client.registerDevice(request)
// Store in Keychain -- never returned again
try KeychainHelper.save(key: response.apiKey)Each platform requires specific permissions before enforcement can begin.
import FamilyControls
// Request FamilyControls authorization
let center = AuthorizationCenter.shared
try await center.requestAuthorization(for: .individual)
// Enable push notifications for policy refresh
let settings = await UNUserNotificationCenter.current().notificationSettings()
if settings.authorizationStatus != .authorized {
try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound])
}
UIApplication.shared.registerForRemoteNotifications()Once permissions are granted and the device key is stored, start the policy sync loop.
let deviceKey = try KeychainHelper.load()!
let config = PhosraConfiguration(deviceKey: deviceKey)
let client = PhosraAPIClient(configuration: config)
let syncManager = PolicySyncManager(client: client)
syncManager.startPolling(interval: 300) // 5 minutesA policy goes through these stages:
Create Generate Activate Push to Device Enforce Report
+--------+ +----------+ +----------+ +--------------+ +---------+ +--------+
| Draft | -> | Rules | -> | Active | -> | Device Fetch | -> | Applied | -> | Status |
| | | Generated| | | | (poll/push) | | on-device| | Report |
+--------+ +----------+ +----------+ +--------------+ +---------+ +--------+
| |
+--- Parent modifies rule -----> Push notification
const policy = await phosra.policies.create(childId, {
name: "Emma's School Year Policy"
});await phosra.policies.generateFromAge(policy.id);
// Generates up to 45 rules based on the child's age groupawait phosra.policies.activate(policy.id);
// Policy version increments, triggering device syncThe device SDK calls GET /device/policy and receives a CompiledPolicy with all rules translated into an enforceable format (content filters, screen time, web filters, etc.).
The enforcement engine maps each policy section to native OS APIs and applies the restrictions.
The device submits an enforcement status report listing per-category results (enforced, partial, failed, unsupported).
| Capability | iOS | Android |
|---|---|---|
| Content filtering | ManagedSettings (age rating + app blocking) | UsageStatsManager + Overlay |
| Screen time limits | DeviceActivity (native schedules) | UsageStatsManager + AlarmManager |
| Web filtering | ManagedSettings (webContent) | Local VPN (DNS interception) |
| Purchase controls | ManagedSettings (appStore) | DevicePolicyManager |
| App blocking | ManagedSettings (blockedApplications) | AccessibilityService + Overlay |
| Notification control | ManagedSettings (iOS 16.4+) | NotificationListenerService |
| Background sync | APNs silent push + Timer | WorkManager + FCM |
| Key storage | Keychain | EncryptedSharedPreferences |
| Min OS version | iOS 16.0 | Android 8.0 (API 26) |
| Special entitlements | FamilyControls entitlement (Apple approval) | Permissions Declaration Form (Play Console) |
| Policy refresh | APNs silent push + polling | FCM data message + WorkManager |
When a child crosses an age bracket boundary (e.g., turning 13), the parent may want to adjust their policy. Phosra makes this straightforward:
The Phosra API knows each child's birth date and automatically computes the age group. When the age group changes, a webhook event is fired:
{
"event": "child.age_group_changed",
"data": {
"child_id": "uuid",
"previous_age_group": "tween",
"new_age_group": "teen",
"child_age": 13
}
}Call generateFromAge to update rules for the new age group:
// Regenerate rules based on new age
await phosra.policies.generateFromAge(policyId);
// Activate to push to device
await phosra.policies.activate(policyId);For a smoother transition, adjust specific rules rather than regenerating everything:
// Allow social media for a new teen
await phosra.rules.update(socialMediaRule.id, {
enabled: true,
config: { mode: 'friends_only' }
});
// Increase daily screen time limit
await phosra.rules.update(dailyLimitRule.id, {
config: { daily_minutes: 180 }
});A single child can have multiple devices registered, and each receives the same compiled policy.
Child "Emma"
├── iPad (iOS) ─── Device Key A ─── FamilyControls enforcement
├── iPhone (iOS) ── Device Key B ─── FamilyControls enforcement
└── Pixel (Android) ── Device Key C ── VPN + UsageStats enforcement
Each device:
Screen time is tracked per-device. To enforce a combined limit across devices, use the Phosra API to aggregate:
// Server-side: sum usage across all devices
const devices = await phosra.devices.list(childId);
let totalMinutes = 0;
for (const device of devices) {
const reports = await getLatestScreenTimeReport(device.id);
totalMinutes += reports.totalMinutes;
}
// If combined usage exceeds limit, update policy to lock remaining devices
if (totalMinutes >= policy.screenTime.dailyLimitMinutes) {
// Push a locked policy to remaining active devices
}curl https://phosra-api.fly.dev/api/v1/children/CHILD_UUID/devices \
-H "Authorization: Bearer $PHOSRA_API_KEY"Both SDKs cache the most recent compiled policy on-device, enabling enforcement even without network connectivity.
When a policy is fetched, the SDK stores it locally:
On app launch or device reboot, the enforcement engine loads the cached policy and re-applies it immediately.
When network returns, the SDK syncs with the API and applies any updates.
Device Boot / App Launch
│
├── Load cached policy from local storage
├── Apply enforcement immediately (no network needed)
│
└── Background: attempt API sync
│
├── Success: update cache, re-apply if version changed
└── Failure: continue with cached policy, retry later
| Platform | Storage | Protection |
|---|---|---|
| iOS | Keychain | Hardware-backed, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly |
| Android | EncryptedSharedPreferences | AES-256 via Android Keystore |
The device API key is:
X-Device-Key header for all device-authenticated API callsPrevent the child from bypassing enforcement:
since_version) minimizes data transfer