Skip to main content
3Nsofts logo3Nsofts
iOS Architecture

The Company App: Offline-First iOS Case Study for SME Operations

The Company App is an offline-first iOS business operations tool for a distribution company with warehouse and office teams. This case study covers the architectural constraints, the Core Data + CloudKit sync strategy, UUID-keyed data model, role-based access, and how the app replaced spreadsheets and messaging apps in a multi-user, multi-device environment.

By Ehsan Azish · 3NSOFTS·May 2026·9 min read

The Problem: Warehouse and Office Coordination at Scale

A distribution business runs on coordination. Warehouse staff receive deliveries, pull orders, and track inventory. Office staff manage schedules, handle customer enquiries, and reconcile accounts. Both teams need access to the same data — and they are rarely in the same room, and almost never on the same network.

The client's situation before building The Company App: warehouse team on one group chat, office team on another, inventory tracked in a shared spreadsheet that was out of date by noon every day. Delivery confirmations came in over messaging apps. Scheduling conflicts were resolved by phone call.

This is not an unusual situation. It is how most small distribution businesses operate until the coordination overhead becomes the bottleneck.


Client Conditions

  • Distribution business, 12–18 staff across two sites
  • Mix of iPhone and iPad across warehouse and office teams
  • Shared data accessed by multiple users simultaneously — no single source of truth
  • Unreliable warehouse Wi-Fi — devices must read and write data without network dependency
  • No existing backend infrastructure and no budget to build and maintain one
  • Required: role differentiation between warehouse staff and office staff

These constraints eliminated several common approaches:

  • Firebase / Supabase — cloud-dependent backends require a network connection for reads and writes
  • Custom REST API — no budget for backend infrastructure; ongoing hosting and maintenance cost
  • Web app — offline capability is a first-class requirement; a web app with service workers can approximate offline behaviour but with significant complexity

The correct solution is local-first persistence with CloudKit sync: Core Data as the on-device source of truth, NSPersistentCloudKitContainer for background synchronisation.


Architecture

Data Layer: Core Data with NSPersistentCloudKitContainer

NSPersistentCloudKitContainer is a drop-in replacement for NSPersistentContainer that adds automatic CloudKit synchronisation. The app writes to Core Data. CloudKit sync happens in the background. Reads always come from the local store — the app never blocks on CloudKit availability.

class DataController: ObservableObject {
    let container: NSPersistentCloudKitContainer

    init() {
        container = NSPersistentCloudKitContainer(name: "CompanyModel")

        container.persistentStoreDescriptions.first?.setOption(
            true as NSNumber,
            forKey: NSPersistentHistoryTrackingKey
        )
        container.persistentStoreDescriptions.first?.setOption(
            true as NSNumber,
            forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey
        )

        container.loadPersistentStores { storeDescription, error in
            if let error {
                fatalError("Core Data store failed: \(error)")
            }
        }

        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }
}

automaticallyMergesChangesFromParent ensures that background CloudKit sync operations — which land in the background context — propagate to the view context without manual notification handling.

UUID Primary Keys for Conflict-Safe Merging

Every entity in the data model uses a UUID as its primary key. This is not optional for CloudKit sync — integer auto-increment keys produce conflicts when multiple devices create records simultaneously.

@Model
class DeliveryRecord {
    var id: UUID
    var referenceNumber: String
    var status: DeliveryStatus
    var assignedTo: UUID  // Staff member UUID
    var createdAt: Date
    var updatedAt: Date

    init(referenceNumber: String) {
        self.id = UUID()
        self.referenceNumber = referenceNumber
        self.status = .pending
        self.assignedTo = UUID()  // Placeholder, assigned later
        self.createdAt = Date()
        self.updatedAt = Date()
    }
}

The UUID key means that a record created by the warehouse team on an iPad and a record created by the office team on an iPhone cannot produce a primary key collision, regardless of whether those devices were online when the records were created.

Role-Based Access

Role is stored as a Core Data entity linked to the device's iCloud account identifier. This allows:

  • The same iCloud account to have different roles in different company instances
  • Role changes to propagate to all devices via CloudKit sync without requiring app reinstall
  • View-layer access control based on the current user's role without a server round-trip
enum StaffRole: String, Codable {
    case warehouse
    case office
    case admin
}

@Observable
class SessionManager {
    var currentRole: StaffRole?

    func resolveRole(for iCloudID: String) async {
        let role = await roleRepository.fetchRole(for: iCloudID)
        self.currentRole = role
    }
}

SwiftUI with iPhone and iPad Adaptive Layout

The app is a single SwiftUI codebase. On iPhone, navigation is stack-based — a list view navigates to a detail view. On iPad, a NavigationSplitView presents the list and detail simultaneously in a two-column layout.

struct ContentView: View {
    @State private var selectedItem: DeliveryRecord?

    var body: some View {
        NavigationSplitView {
            DeliveryListView(selection: $selectedItem)
        } detail: {
            if let item = selectedItem {
                DeliveryDetailView(record: item)
            } else {
                ContentUnavailableView(
                    "Select a delivery",
                    systemImage: "shippingbox"
                )
            }
        }
    }
}

NavigationSplitView collapses to a stack-based navigation automatically on iPhone. No device detection, no conditional layout code.


Results

Offline read/write: fully functional with no network connection

Sync latency: CloudKit changes propagate to other devices in 2–15 seconds when connectivity is available

Concurrent users: tested with 8 simultaneous active users, no merge conflicts or data loss in 6 months of production use

Device support: iPhone 13+, iPad (6th generation+), iOS 17+

Role-based access: three roles, configurable per-user via admin panel

Spreadsheet replacement: the shared inventory spreadsheet was retired on day 3 of deployment. The group chats are still used for informal communication but no longer carry operational data.

App Store first-submission approval: passed on first submission


Download and Explore

The Company App is available on the App Store for iPhone and iPad.

Download The Company App on the App Store →

The Company App product page →

If your business has a similar coordination challenge — multiple teams, mixed connectivity, shared operational data — this architecture scales to your requirements.


Related Reading