Noma
Swift · iOS · Mobile CMS · 2026

CMS for Swift

Noma gives Swift teams a secure API-first content workflow for iOS apps, with backend-safe CMS access, localized delivery, and release-safe publishing without waiting for binary deployments.

Positioning

Why Swift teams use Noma

iOS teams need reliable APIs, strict transport security, and safe content rollout controls. Noma provides structured content, localization, and publish/version workflows while your Swift app focuses on native UX and performance.

This guide is aligned with current Apple documentation for URLSession, ATS, Keychain, BackgroundTasks, and Codable data modeling.

Architecture

Use a backend boundary for CMS access

Do not embed privileged CMS API keys in iOS binaries. Put Noma access in your backend and expose iOS-safe endpoints with scoped payloads.

// backend/lib/noma.ts
import { createClient } from "@nomacms/js-sdk";
 
export function getNomaServerClient() {
  const projectId = process.env.NOMA_PROJECT_ID;
  const apiKey = process.env.NOMA_API_KEY;
  if (!projectId || !apiKey) throw new Error("Missing Noma env vars");
  return createClient({ projectId, apiKey });
}
Networking

URLSession + Codable over ATS-secure transport

Apple recommends high-level networking APIs and ATS-secure connections. Use URLSession and Codable to parse backend JSON into typed Swift models.

import Foundation
 
struct FeedItem: Codable {
  let uuid: String
  let title: String
  let summary: String?
}
 
struct FeedResponse: Codable {
  let items: [FeedItem]
}
 
final class MobileAPIClient {
  private let session: URLSession
  private let baseURL = URL(string: "https://api.example.com")!
 
  init(session: URLSession = .shared) {
    self.session = session
  }
 
  func fetchHomeFeed(locale: String) async throws -> [FeedItem] {
    var components = URLComponents(
      url: baseURL.appendingPathComponent("/mobile/home-feed"),
      resolvingAgainstBaseURL: false
    )!
    components.queryItems = [URLQueryItem(name: "locale", value: locale)]
 
    let request = URLRequest(url: components.url!)
    let (data, response) = try await session.data(for: request)
 
    guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else {
      throw URLError(.badServerResponse)
    }
 
    return try JSONDecoder().decode(FeedResponse.self, from: data).items
  }
}

Keep ATS enabled by default and prefer fixing server TLS configuration rather than broad NSAllowsArbitraryLoads exceptions.

Security

Store tokens in Keychain

Keychain services are the default secure storage path for secrets on Apple platforms. Keep auth/session secrets out of UserDefaults.

import Foundation
import Security
 
enum TokenStore {
  static func save(token: String, account: String = "noma_access_token") throws {
    let data = Data(token.utf8)
    let query: [String: Any] = [
      kSecClass as String: kSecClassGenericPassword,
      kSecAttrAccount as String: account,
      kSecValueData as String: data
    ]
    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else { throw URLError(.cannotCreateFile) }
  }
}
Offline-first

Repository pattern for fast app startup

Use local-first reads and background refresh for resilient mobile UX under variable network conditions. Noma list/get patterns map cleanly to this repository model.

import Foundation
 
final class HomeFeedRepository {
  private let api: MobileAPIClient
  private let cache: FeedCache
 
  init(api: MobileAPIClient, cache: FeedCache) {
    self.api = api
    self.cache = cache
  }
 
  // Local-first read, then async refresh.
  func get(locale: String) async throws -> [FeedItem] {
    let cached = try cache.read(locale: locale)
    if !cached.isEmpty {
      Task { try? await refresh(locale: locale) }
      return cached
    }
    return try await refresh(locale: locale)
  }
 
  func refresh(locale: String) async throws -> [FeedItem] {
    let fresh = try await api.fetchHomeFeed(locale: locale)
    try cache.write(fresh, locale: locale)
    return fresh
  }
}
Background sync

Use BackgroundTasks for refresh workflows

For periodic content refresh and maintenance, schedule background work with BackgroundTasks and update local cache when tasks run.

// App startup:
// BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.refresh", using: nil) { task in ... }
//
// Schedule refresh request with BGAppRefreshTaskRequest and
// perform API sync + cache update when system launches task.
Release operations

Publish and rollback without app redeploys

Content for banners, onboarding, and pricing can be updated through Noma draft/publish/version controls independently of App Store release timing.

// backend/content-release.ts
const noma = getNomaServerClient();
 
await noma.content.patch("ios_config", configUuid, {
  data: { promo_banner: "Summer campaign" },
});
 
await noma.content.publish("ios_config", configUuid);
 
const versions = await noma.content.versions.list("ios_config", configUuid);
await noma.content.versions.revert("ios_config", configUuid, versions[0].uuid);
Automation

Swift mobile content ops with MCP

Use @nomacms/mcp-server and workflows from Agent Skills to automate schema updates, localization workflows, and release QA checks.

Next steps

Continue with related guides

Now available

Start building with Noma

Create a free account, spin up a project, and ship structured content with our API, SDK, and AI tools.