Skip to main content
3Nsofts logo3Nsofts
iOS · iPadOS · macOS · Live in ProductionCase Study

The Company App: One Business System Across Every Apple Device

Inventory, sales, purchasing, accounting, contacts, calendar, team chat, and operations—connected in one offline-first workspace for iPhone, iPad, and Mac.

Stack

SwiftUI · Core Data · CloudKit

Platform

iOS 17+ · iPadOS 17+ · macOS 14+

Architecture

Offline-First · Zero Backend

Outcome

6–8 tools → 1 system

Context

Small businesses operating with 8–25 employees across warehouse, office, and field environments face a compounding coordination problem: operations, finances, and team communication live in separate systems that do not talk to each other. Warehouse staff track stock in a spreadsheet. Finance runs another for accounting. Orders live somewhere else. Team communication happens in messaging apps. Dispatch is logged on paper.

The result is stale data, fulfilment errors, missed payments, and a management layer that spends its time reconciling systems instead of running the business. No single source of truth exists.

Problem

The team operated across 6–8 tools: spreadsheets for inventory and accounting, messaging apps for team coordination, a separate purchase-order system, paper dispatch logs, and email for customer communication. No tool communicated with another. Finance could not see order status, warehouse staff could not see what was on order, and dispatch did not know what had been received.

The instinct is to solve this with a web platform and a shared database. The problem: warehouse connectivity is unreliable. A system that requires a server connection to function would fail at the point where it matters most — on the warehouse floor during a stock count or dispatch run.

Constraints

  • Warehouse connectivity is unreliable — fully offline operation was required, with automatic sync on reconnection
  • Multi-user concurrent writes required conflict resolution without a custom backend
  • Sensitive business data (supplier contacts, pricing, order volumes) could not transit third-party services
  • No IT department on the client side — zero-maintenance sync infrastructure was a hard requirement
  • iPad form factor needed to work in warehouse scanning contexts: large tap targets, minimal navigation depth

Architecture Approach

The constraint that shaped everything: warehouse staff cannot wait for a server response. Offline-first was not a feature request — it was the design premise. Every architectural decision flows from that.

Data Layer

NSPersistentCloudKitContainer was the clear choice: a single class that wraps Core Data, automatically mirrors to CloudKit, and requires no backend infrastructure. Writes go to local Core Data first — the app is fully functional offline. Sync happens transparently when connectivity is available.

The schema covers the full operational surface: inventory items, stock movements, orders, order line items, dispatch assignments, customer contacts, and team tasks — all in one unified Core Data model. This means any view in the app can compose across domains without network calls.

CloudKit Store Design

Two CloudKit stores run in parallel. The private store holds per-user settings and preferences — it syncs only to the authenticated user's iCloud account. The shared store holds the operational data — inventory, orders, dispatch — accessible to all team members via CloudKit Sharing participant roles.

Role-based access control is enforced at the persistence layer through CloudKit participant roles (owner, contributor, viewer), not at the UI layer. A warehouse worker cannot modify order pricing; the data model makes it structurally impossible, not just hidden.

Conflict Resolution

Concurrent offline writes from multiple users are resolved through Core Data's merge policy. The app uses NSMergeByPropertyObjectTrumpMergePolicy for most records — last-writer-by-timestamp wins at the property level. For inventory quantities (where concurrent increments/decrements must not overwrite each other), stock movements are recorded as individual events rather than mutating a single quantity field. The current quantity is derived by summing all movement records — a log-structured approach that makes concurrent writes commutative.

Implementation: Offline-Aware UI State

Every view that shows synced data needs to communicate its sync state to the user — not as a banner, but inline. The pattern used across the app surfaces pending changes count and last-sync timestamp as lightweight metadata attached to each list item:

// Observe CloudKit sync events via NotificationCenter
// and surface pending record count to UI without blocking

class SyncMonitor: ObservableObject {
    @Published var pendingCount: Int = 0
    @Published var lastSyncDate: Date?

    init(container: NSPersistentCloudKitContainer) {
        NotificationCenter.default.addObserver(
            forName: NSPersistentCloudKitContainer
                .eventChangedNotification,
            object: container,
            queue: .main
        ) { [weak self] notification in
            guard let event = notification.userInfo?[
                NSPersistentCloudKitContainer.eventNotificationUserInfoKey
            ] as? NSPersistentCloudKitContainer.Event else { return }

            if event.succeeded {
                self?.lastSyncDate = event.endDate
            }
        }
    }
}

// In SwiftUI view: show offline badge when pending > 0
struct InventoryRowView: View {
    let item: InventoryItem
    @ObservedObject var syncMonitor: SyncMonitor

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(item.name)
                Text("\(item.quantity) units")
                    .foregroundStyle(.secondary)
            }
            Spacer()
            if syncMonitor.pendingCount > 0 {
                Image(systemName: "arrow.triangle.2.circlepath")
                    .foregroundStyle(.orange)
                    .imageScale(.small)
            }
        }
    }
}

Solution Highlights

  • NSPersistentCloudKitContainer — zero-backend sync through Apple infrastructure, no server costs
  • Separate private/shared CloudKit stores with participant-role enforcement at the persistence layer
  • Unified schema for inventory, sales, purchase orders, accounting, dispatch, contacts, calendar, and chat
  • Accounting figures derived automatically from sales, purchase, receipt, and expense records
  • Team Chat and shared calendar synced through the same CloudKit container as operational data
  • Operations hub for cargo, warehouse assignments, dispatches, returns, defects, and expenses
  • Log-structured stock movements for conflict-safe concurrent inventory updates
  • Partial sync: field staff receive only their assigned dispatch subset via CloudKit record zones
  • iPad split-view layouts with large tap targets and offline-aware status indicators
  • Zero third-party dependencies — every SDK is a first-party Apple framework

CloudKit Sharing and Role Enforcement

Team members join through an iCloud sharing invitation. Their access level—contributor or viewer—is enforced through CloudKit participant roles at the persistence layer, not only by hiding controls in the interface.

The owner’s private store and shared company store use separate NSPersistentCloudKitContainer configurations. Operational data lives in the shared store while personal preferences remain private. Once a participant accepts, the shared zone mirrors to their device automatically, with no separate account system or sign-in flow beyond iCloud.

Implementation detail

UICloudSharingController must be presented from the topmost view controller. Presenting from a root controller that already owns a sheet can fail silently, so the app walks the presentedViewController chain before presentation.

Outcome

Deployed across teams of 8–15 employees managing inventory at multiple warehouse locations. The system handles offline operation for hours during connectivity outages and syncs automatically on reconnection — no manual intervention required.

  • 6–8 operational tools consolidated into one system with a single source of truth
  • Accounting figures derived from sales and purchase records with no duplicate entry
  • Communication, calendar, and operational data synced across iPhone, iPad, and Mac
  • Offline operation during warehouse connectivity outages: hours of uninterrupted use
  • Zero infrastructure to maintain — CloudKit handles sync, Apple handles availability
  • No server costs: CloudKit storage is included in iCloud subscriptions the business already pays

“The constraint that shaped the entire architecture: warehouse staff cannot wait for a server response. Offline-first was not a feature — it was the design premise.”

Key Technical Learnings

Model mutations as events, not state

For any field that multiple users might change concurrently (stock quantities, order status), record the delta events rather than mutating a single value. Events are commutative; property overwrites are not.

NSPersistentCloudKitContainer is not a magic sync layer

Conflict resolution requires deliberate schema design. Records with large blob properties (images, PDFs) should live in separate entities to avoid CloudKit's record size limits becoming merge-time failures.

Offline-aware UI is not optional

Users who don't know they're offline will assume the data they see is current. A subtle sync-state indicator on every list item — not a banner — keeps the mental model accurate without adding cognitive overhead.

Partial sync requires CloudKit zones

CloudKit's record zone model is the right primitive for scoping data to individual users. Design your zone structure before your entity structure — retrofitting zones into an existing schema is painful.

Accounting is a view over operational data

Receivables, payables, cash position, and profit and loss are derived from the sales and purchase records that already drive operations. A normalized model prevents accounting totals drifting away from business reality.

Technical FAQ

How does offline-first sync work with NSPersistentCloudKitContainer?
All writes go to the local Core Data store first — the app works fully offline. When connectivity is restored, CloudKit reconciles changes using merge policies you define at the container level. Conflicts resolve at the record level using last-write-wins by timestamp, unless you implement custom merge logic for specific entity types.
Why choose NSPersistentCloudKitContainer over a custom backend?
For a small team with no IT staff, operational complexity is a product risk. A custom backend adds infrastructure costs, maintenance burden, and a new failure point. CloudKit gives you a managed, Apple-hosted sync layer where the SLA is Apple's — not yours to maintain.
What is the performance cost of offline-first Core Data?
Near-zero for reads. Core Data's in-memory store cache means most reads never hit disk. Writes are fast because they go to SQLite locally; CloudKit sync happens asynchronously in the background. The only visible latency is when a user with a large schema first installs — the initial CloudKit mirror import.
SwiftUICore DataCloudKitNSPersistentCloudKitContainerOffline-FirstiOS 17+iPadOS 17+macOS 14+

Building something with similar constraints?

If you need an offline-first iOS system with multi-user sync, an architecture audit can identify the right data layer approach before you commit to a design.