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.mddelta-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.)
| Stage | Model | Notes |
|---|---|---|
| MVP | Sonnance holds all — MongoDB + Azure Blob under our account | Simple, controlled. |
| Near-term | + Data export ("download everything": files + metadata manifest) | Cheap, high-trust; churn-proofing + GDPR-style portability. Low effort. |
| Enterprise lever | BYO storage bucket — a workspace connects its own Azure Blob / S3; Sonnance keeps metadata, heavy audio lives in the customer's bucket | Already 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-term | Self-hosted / on-prem — the whole backend runs in the customer's cloud | Top 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.appas 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 Groupgroup.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), makeAPPLE_CLIENT_IDa list and pass it as theaudiencearray toappleSignin.verifyIdToken(it acceptsstring | 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 maintainscreatedAt/updatedAtautomatically (ArtistSpace, Project, Track, Comment, Team, TeamShare, ShareTag, …). Gap: a few schemas may not settimestamps— audit and enable uniformly. - CreatedBy → partial. Every owned resource carries an
ownerref (≈ 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):
- A small reusable Mongoose plugin adding
createdBy/modifiedBy(ObjectId → User), applied across schemas alongsidetimestamps. - The write path needs the authenticated user at save time. Cleanest: a request-scoped provider (or an interceptor) that stamps
modifiedBy(andcreatedByon insert) from@GetUser(). Mongoose middleware alone can't see the request user, so the user must be threaded in (service param or AsyncLocalStorage). - Optional: a richer activity/audit log (append-only
{ actor, action, entity, before/after, at }) if true change-history is needed — heavier; theComment/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.