Skip to main content
The PhosraSDK Swift package provides on-device child safety enforcement for iOS using Apple’s FamilyControls, ManagedSettings, and DeviceActivity frameworks.

Overview

The SDK has a two-layer architecture:
  1. API Client — Communicates with the Phosra API to register devices, fetch compiled policies, submit enforcement reports, and acknowledge policy versions.
  2. Enforcement Engine — Translates the compiled policy document into native Apple framework calls to enforce content filters, screen time limits, web filtering, purchase controls, and more.

API Client

Handles device registration, policy sync, and reporting via the Phosra REST API.

Enforcement Engine

Maps policy rules to FamilyControls, ManagedSettings, and DeviceActivity calls.

Requirements

RequirementVersion
iOS16.0+
Swift5.9+
Xcode15+
FamilyControls EntitlementRequired (request here)
The FamilyControls entitlement must be approved by Apple before your app can enforce restrictions on-device. Request it early — approval typically takes 1-2 weeks.

Installation

The PhosraSDK Swift package is in private preview. Contact developers@phosra.com to receive the repository URL and access credentials. Once you have access, add it via Swift Package Manager:
Xcode
File > Add Package Dependencies
URL: <repo URL provided by Phosra>
Product: PhosraSDK

Quick Start

1
Request FamilyControls Authorization
2
The child’s device must authorize FamilyControls before any enforcement can occur:
3
import FamilyControls

let center = AuthorizationCenter.shared

do {
    try await center.requestAuthorization(for: .individual)
    print("FamilyControls authorized")
} catch {
    print("Authorization denied: \(error)")
}
4
Register the Device
5
The parent initiates device registration from their authenticated session. This requires a parent JWT token:
6
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)
7
Store the API Key in Keychain
8
The API key is returned exactly once during registration. The server only stores a SHA-256 hash; the plaintext is never returned again:
9
try KeychainHelper.save(key: response.apiKey)
10
Fetch and Apply the Policy
11
Switch to device-authenticated mode and fetch the compiled policy:
12
let deviceKey = try KeychainHelper.load()!
let deviceConfig = PhosraConfiguration(deviceKey: deviceKey)
let deviceClient = PhosraAPIClient(configuration: deviceConfig)

let policy = try await deviceClient.fetchPolicy()
print("Applying policy v\(policy.version) for \(policy.ageGroup)")

// Apply via enforcement engine
let engine = EnforcementEngine()
let results = try await engine.apply(policy)

// Report results back to Phosra
let report = DeviceReport(
    reportType: .enforcementStatus,
    payload: .enforcementStatus(EnforcementStatusPayload(
        policyVersion: policy.version,
        results: results
    ))
)
try await deviceClient.submitReport(report)
try await deviceClient.ackPolicyVersion(policy.version)

Device Registration

The registration flow requires parent authentication and produces a device-specific API key:
Parent App                    Phosra API                  Child Device
    │                             │                           │
    ├── POST /children/{id}/devices ──────────────────────>   │
    │   (Authorization: Bearer parentJWT)                     │
    │                             │                           │
    │   <── { device, api_key } ──┤                           │
    │                             │                           │
    │   ── Transfer api_key ──────────────────────────────>   │
    │                             │                           │ Store in Keychain
    │                             │                           │
The RegisterDeviceRequest includes device metadata and capability declarations:
RegisterDeviceRequest(
    deviceName: "Emma's iPad",
    deviceModel: "iPad Pro 11-inch",     // UIDevice.current.model
    osVersion: "17.4.1",                 // UIDevice.current.systemVersion
    appVersion: "1.0.0",                 // Bundle version
    capabilities: [
        "FamilyControls",               // Core entitlement
        "ManagedSettings",              // App/web blocking
        "DeviceActivity",               // Usage monitoring
        "WebContentFilter"              // Network extension filtering
    ],
    apnsToken: apnsDeviceToken           // Optional, for push-based sync
)

Policy Sync

Automatic Polling with PolicySyncManager

The PolicySyncManager handles periodic policy fetching with conditional requests:
class PolicySyncManager {
    private let client: PhosraAPIClient
    private let engine: EnforcementEngine
    private var currentVersion: Int = 0
    private var timer: Timer?

    /// Start polling for policy updates.
    /// Default interval: 5 minutes (300 seconds).
    func startPolling(interval: TimeInterval = 300) {
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            Task { await self?.sync() }
        }
    }

    func stopPolling() {
        timer?.invalidate()
        timer = nil
    }

    private func sync() async {
        do {
            // Conditional fetch -- returns nil on 304 Not Modified
            guard let policy = try await client.fetchPolicy(sinceVersion: currentVersion) else {
                return // No update
            }

            currentVersion = policy.version
            let results = try await engine.apply(policy)

            // Report enforcement results
            let report = DeviceReport(
                reportType: .enforcementStatus,
                payload: .enforcementStatus(EnforcementStatusPayload(
                    policyVersion: policy.version,
                    results: results
                ))
            )
            try await client.submitReport(report)
            try await client.ackPolicyVersion(policy.version)
        } catch {
            print("Policy sync failed: \(error)")
        }
    }
}

Push-Based Sync (APNs)

For immediate policy refresh when a parent modifies rules, handle APNs silent push notifications:
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any]
) async -> UIBackgroundFetchResult {
    guard let phosra = userInfo["phosra"] as? [String: Any],
          let event = phosra["event"] as? String,
          event == "policy.updated",
          let newVersion = phosra["version"] as? Int else {
        return .noData
    }

    if newVersion > currentPolicyVersion {
        do {
            let policy = try await client.fetchPolicy()
            try await engine.apply(policy)
            try await client.ackPolicyVersion(policy.version)
            return .newData
        } catch {
            return .failed
        }
    }
    return .noData
}
The push notification payload:
{
  "aps": { "content-available": 1 },
  "phosra": {
    "event": "policy.updated",
    "version": 4
  }
}

Enforcement Engine

Each enforcer handles a specific policy section and maps it to the appropriate Apple framework.

Content Filter Enforcer

Uses ManagedSettings to control app access based on age ratings and block/allow lists.
Policy FieldApple FrameworkAPI
contentFilter.ageRatingManagedSettingsManagedSettingsStore.application
contentFilter.blockedAppsManagedSettingsblockedApplications
contentFilter.allowlistModeManagedSettingsdenyAppInstallation

Screen Time Enforcer

Uses DeviceActivity to monitor and limit screen time.
Policy FieldApple FrameworkAPI
screenTime.dailyLimitMinutesDeviceActivityDeviceActivitySchedule
screenTime.perAppLimitsDeviceActivityPer-token schedule
screenTime.downtimeWindowsDeviceActivityDeviceActivitySchedule
screenTime.scheduleDeviceActivityDateComponents ranges

Web Filter Enforcer

Uses ManagedSettings web content filtering.
Policy FieldApple FrameworkAPI
webFilter.safeSearchManagedSettingswebContent.autoFilter
webFilter.blockedDomainsManagedSettingswebContent.blockedDomains
webFilter.allowedDomainsManagedSettingswebContent.allowedDomains
webFilter.levelManagedSettingswebContent filter mode

Purchase Enforcer

Policy FieldApple FrameworkAPI
purchases.requireApprovalManagedSettingsappStore.requirePasswordForPurchases
purchases.blockIAPManagedSettingsappStore.denyInAppPurchases
purchases.spendingCapUSDNoneTracked via StoreKit observer

Social Enforcer

Policy FieldApple FrameworkAPI
social.multiplayerModeManagedSettingsgameCenter.denyMultiplayerGaming
social.chatModeNoneBlock messaging apps via ManagedSettings
social.dmRestrictionNoneBlock messaging apps via ManagedSettings

Notification Enforcer

Policy FieldApple FrameworkAPI
notifications.curfewStart/EndManagedSettingsnotification (iOS 16.4+)
notifications.usageTimerMinutesDeviceActivityDeviceActivityMonitor events

Reporting

The SDK submits two types of reports to the Phosra API:

Enforcement Status Report

Sent after applying a policy, contains per-category results:
DeviceReport(
    reportType: .enforcementStatus,
    payload: .enforcementStatus(EnforcementStatusPayload(
        policyVersion: 3,
        results: [
            CategoryEnforcementResult(
                category: "content_rating",
                status: "enforced",       // "enforced", "partial", "failed", "unsupported"
                framework: "ManagedSettings"
            ),
            CategoryEnforcementResult(
                category: "time_daily_limit",
                status: "enforced",
                framework: "DeviceActivity"
            )
        ]
    ))
)

Screen Time Report

Aggregated daily usage data:
DeviceReport(
    reportType: .screenTime,
    payload: .screenTime(ScreenTimePayload(
        byCategory: ["social-media": 45, "gaming": 30, "education": 60],
        topApps: ["com.burbn.instagram": 25, "com.google.ios.youtube": 20],
        totalMinutes: 135,
        date: "2026-02-23"
    ))
)

CompiledPolicy Structure

The compiled policy is fetched from GET /device/policy and contains all enforcement rules organized by section:
{
  "version": 3,
  "child_id": "uuid",
  "child_age": 10,
  "age_group": "tween",
  "policy_id": "uuid",
  "status": "active",
  "generated_at": "2026-02-23T14:30:00Z",
  "content_filter": {
    "age_rating": "9+",
    "max_ratings": { "apple": "9+", "mpaa": "PG" },
    "blocked_apps": [],
    "allowed_apps": [],
    "allowlist_mode": false
  },
  "screen_time": {
    "daily_limit_minutes": 120,
    "per_app_limits": [],
    "downtime_windows": [
      { "days_of_week": ["monday","tuesday","wednesday","thursday","friday"],
        "start_time": "21:00", "end_time": "07:00" }
    ],
    "always_allowed_apps": ["com.apple.mobilephone", "com.apple.MobileSMS", "com.apple.Maps"],
    "schedule": { "weekday": { "start": "07:00", "end": "21:00" },
                  "weekend": { "start": "08:00", "end": "22:00" } }
  },
  "purchases": {
    "require_approval": true,
    "block_iap": false,
    "spending_cap_usd": 10.00
  },
  "privacy": {
    "location_sharing_enabled": true,
    "profile_visibility": "private",
    "account_creation_approval": true,
    "data_sharing_restricted": true
  },
  "social": {
    "chat_mode": "friends_only",
    "dm_restriction": "contacts_only",
    "multiplayer_mode": "friends_only"
  },
  "notifications": {
    "curfew_start": "21:00",
    "curfew_end": "07:00",
    "usage_timer_minutes": 30
  },
  "web_filter": {
    "level": "moderate",
    "safe_search": true,
    "blocked_domains": ["reddit.com"],
    "allowed_domains": ["khanacademy.org"],
    "blocked_categories": ["gambling", "adult"]
  }
}

Supported Rule Categories

The 45 OCSS rule categories the iOS SDK maps to native framework primitives (of the 123 categories in the OCSS rule registry; the remainder are policy-only or census-enforced and carry no client-side Apple verb):

Content Rules

CategoryApple FrameworkAPI ClassMin iOS
content_ratingManagedSettingsManagedSettingsStore.application16.0
content_block_titleManagedSettingsblockedApplications16.0
content_allow_titleManagedSettingsdenyAppInstallation16.0
content_allowlist_modeManagedSettingsdenyAppInstallation16.0
content_descriptor_blockManagedSettingsManagedSettingsStore.application16.0

Time Rules

CategoryApple FrameworkAPI ClassMin iOS
time_daily_limitDeviceActivityDeviceActivitySchedule16.0
time_scheduled_hoursDeviceActivityDeviceActivitySchedule16.0
time_per_app_limitDeviceActivityDeviceActivitySchedule16.0
time_downtimeDeviceActivityDeviceActivitySchedule16.0

Purchase Rules

CategoryApple FrameworkAPI ClassMin iOS
purchase_approvalManagedSettingsappStore.requirePasswordForPurchases16.0
purchase_block_iapManagedSettingsappStore.denyInAppPurchases16.0
purchase_spending_capNoneStoreKit observer (tracked in-app)

Social Rules

CategoryApple FrameworkAPI ClassMin iOS
social_contactsFamilyControlsAuthorizationCenter16.0
social_chat_controlNoneBlock messaging apps via ManagedSettings
social_multiplayerManagedSettingsgameCenter.denyMultiplayerGaming16.0

Web Rules

CategoryApple FrameworkAPI ClassMin iOS
web_safesearchManagedSettingswebContent.autoFilter16.0
web_category_blockManagedSettingswebContent.blockedByFilter16.0
web_custom_allowlistManagedSettingswebContent.allowedDomains16.0
web_custom_blocklistManagedSettingswebContent.blockedDomains16.0
web_filter_levelManagedSettingswebContent16.0

Privacy Rules

CategoryApple FrameworkNotes
privacy_locationNoneCLLocationManager delegation in-app
privacy_profile_visibilityNoneApp-level concern
privacy_data_sharingNoneApp-level logic; ATT covers ads only
privacy_account_creationNoneGate via FamilyControls auth check

Monitoring Rules

CategoryApple FrameworkAPI ClassMin iOS
monitoring_activityDeviceActivityDeviceActivityReport16.0
monitoring_alertsDeviceActivityDeviceActivityMonitor16.0

Engagement Rules

CategoryApple FrameworkNotes
algo_feed_controlNoneBlock/limit social apps via ManagedSettings
addictive_design_controlNoneBlock offending apps via ManagedSettings

Notification Rules

CategoryApple FrameworkAPI ClassMin iOS
notification_curfewManagedSettingsnotification16.4
usage_timer_notificationDeviceActivityDeviceActivityMonitor16.0

Legislation-Driven Rules

CategoryApple FrameworkNotes
targeted_ad_blockNoneATT is per-prompt; no blanket block
dm_restrictionNoneBlock messaging apps as proxy
age_gateNoneEnforce in-app
data_deletion_requestNoneApp-level support flow
geolocation_opt_inNoneCLLocationManager per-app

Compliance Rules

CategoryApple FrameworkNotes
csam_reportingNoneApple handles via iCloud Photos
library_filter_complianceManagedSettingsSame as web content filtering
ai_minor_interactionNoneBlock AI apps via ManagedSettings
social_media_min_ageNoneBlock social BundleIDs for underage
image_rights_minorNoneApp-level enforcement

Parental / Legislative Rules

CategoryApple FrameworkAPI ClassMin iOS
parental_consent_gateFamilyControlsAuthorizationCenter16.0
parental_event_notificationDeviceActivityDeviceActivityMonitor16.0
screen_time_reportDeviceActivityDeviceActivityReport16.0
commercial_data_banNoneApp/server-level policy
algorithmic_auditNonePlatform transparency requirement

Troubleshooting

  • Ensure your app has the com.apple.developer.family-controls entitlement
  • The entitlement must be approved by Apple for distribution builds
  • For development, use the automatic provisioning profile in Xcode
  • Check that the device is not already managed by another FamilyControls app
  • Verify FamilyControls authorization status is .approved
  • ManagedSettingsStore changes are transactional; create a new store instance if the previous one has stale state
  • Check the device logs: log stream --predicate 'subsystem == "com.apple.ManagedSettings"'
  • The DeviceActivityMonitor extension must be a separate target in your Xcode project
  • Verify the extension’s bundle ID matches the App Group
  • Maximum of 20 concurrent DeviceActivitySchedule instances per app
This is expected behavior. When you pass sinceVersion, the server returns 304 if the policy has not changed. The SDK returns nil in this case — no action is needed.
  • Ensure Keychain Sharing capability is enabled if using app extensions
  • Use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly (the SDK default) for device keys
  • On simulator, Keychain behavior may differ from physical devices
  • Provide a detailed description of your child safety use case in App Store Connect
  • Include screenshots showing the parent consent flow
  • Reference applicable legislation (COPPA, KOSA, etc.) in your review notes
  • Ensure your privacy policy covers data collected from child devices