Skip to content

Data Architecture, Ownership & Apple Identity

Active spec. Governed by 00-ROADMAP.md. Last updated 2026-05-31.


1. System of record = the Sonnance backend (not CloudKit)

Sonnance is a collaboration platform — multiple people (plus the web app, plus non-Apple users via share links) review the same track. That shared state must be server-authoritative.

  • Source of truth: MongoDB (metadata) + Azure Blob (audio/files), via the NestJS backend.
  • iOS app is a client, not an owner of data. "Backup" is the server — reinstall the app and it re-syncs from the API.
  • CloudKit is NOT used. CloudKit's private DB syncs one Apple ID's data across that user's Apple devices. It cannot power cross-user collaboration, the web app, or Android — using it as the backbone would fork the source of truth. → Remove the Xcode template's SwiftData + CloudKit default.
  • Local cache (later, not MVP): for offline viewing / queued actions, a local store (SwiftData without CloudKit, or GRDB/Core Data) used purely as a cache of server data — see 13-MOBILE-CONSIDERATIONS.md delta-sync. The server stays authoritative (last-write-wins / merge on reconnect).

2. Data-ownership ladder (trust & cost)

Who physically holds the data, staged by maturity. (User stance: MVP no, enterprise later.)

StageModelNotes
MVPSonnance holds all — MongoDB + Azure Blob under our accountSimple, controlled.
Near-term+ Data export ("download everything": files + metadata manifest)Cheap, high-trust; churn-proofing + GDPR-style portability. Low effort.
Enterprise leverBYO storage bucket — a workspace connects its own Azure Blob / S3; Sonnance keeps metadata, heavy audio lives in the customer's bucketAlready feasible: the backend storage layer is abstracted over Azure/S3 (STORAGE_PROVIDER, src/modules/storage/storage.service.ts). The extension is per-tenant storage credentials. Solves "don't want to trust one provider" and "heavy data = heavy Sonnance cost."
Long-termSelf-hosted / on-prem — the whole backend runs in the customer's cloudTop enterprise tier. Heaviest; far future.

MVP decision: Sonnance holds all. Build nothing here yet — but keep the storage/credential layer shaped so BYO-bucket is an additive change, not a rewrite.

3. Apple identity & bundle-id scheme (multi-platform)

  • Keep com.sonnance.app as the primary client id. A universal iOS + macOS app (one SwiftUI multiplatform target / Mac Catalyst) reuses the same id — no rename needed for a Mac version.
  • Namespacing: extensions/companions nest under it — com.sonnance.app.<widget|notification|share>; App Group group.com.sonnance.
  • Only if a separate Mac app is ever split out: com.sonnance.mac. A web "Sign in with Apple" uses a Services ID (e.g. com.sonnance.web) — a different identifier class.
  • Backend impact: the bundle id is the Apple Sign In token aud (= APPLE_CLIENT_ID). The moment a 2nd id exists (Mac/web), make APPLE_CLIENT_ID a list and pass it as the audience array to appleSignin.verifyIdToken (it accepts string | string[]). Single id today → no change now.

Decision: no rename. com.sonnance.app scales via a universal target + namespaced extensions; revisit APPLE_CLIENT_ID → array only when a 2nd Apple identifier appears.

4. Audit / trace-readiness of records (roadmap — owner-raised 2026-06-18)

Goal: Dataverse-style traceability on every entity — CreatedOn / ModifiedOn / CreatedBy / ModifiedBy.

Current state (verified):

  • CreatedOn / ModifiedOn → mostly DONE. Core schemas use @Schema({ timestamps: true }), so Mongoose maintains createdAt / updatedAt automatically (ArtistSpace, Project, Track, Comment, Team, TeamShare, ShareTag, …). Gap: a few schemas may not set timestamps — audit and enable uniformly.
  • CreatedBy → partial. Every owned resource carries an owner ref (≈ original CreatedBy), but it's ownership, not strictly "who first inserted the row" (they usually coincide).
  • ModifiedBy → MISSING. No schema records who made the last change. This is the real gap.

Proposed approach (when scheduled — not now):

  1. A small reusable Mongoose plugin adding createdBy / modifiedBy (ObjectId → User), applied across schemas alongside timestamps.
  2. The write path needs the authenticated user at save time. Cleanest: a request-scoped provider (or an interceptor) that stamps modifiedBy (and createdBy on insert) from @GetUser(). Mongoose middleware alone can't see the request user, so the user must be threaded in (service param or AsyncLocalStorage).
  3. Optional: a richer activity/audit log (append-only { actor, action, entity, before/after, at }) if true change-history is needed — heavier; the Comment/notification trail already covers some of this.

Why deferred: createdAt/updatedAt already give the time axis; ModifiedBy requires touching the write path broadly. Schedule as its own slice (pairs naturally with the doc-33 notes/activity work). No blocker today.

Ctrl-Audio Platform Documentation