Data Flow & Security Architecture

Where your meeting data lives, every path it can take off your Mac, and what we found when we read our own code line by line.

What this document is pinned to

Architecture pages age badly. This one names the exact revision it describes, so you can tell when it has drifted.

Source reviewed
SummitNotes/summit-app
Commit
9b419e07af783e589fe812e1f2b9714b6bd42581
Scope
macOS app (Summit) + iOS companion + shared SummitCore package
Reviewed
30 July 2026

Citations below are paths in that repository at that commit, with line numbers wherever a claim rests on a specific piece of code. A few point at a whole file or a line range instead, because the claim rests on the file as a whole — the entitlements list, the model catalogue — and a single line would be the less honest reference. The repository is private, so the references are for verification during a source review rather than public browsing — we grant read access to enterprise buyers and auditors under NDA. Ask at security@summitnotes.app. Everything else on this page you can verify from the outside with a packet capture: the egress table is the complete list, so any connection you observe that is not on it is a finding, and we want to hear about it.

Trust boundaries

The whole design collapses to one question: what crosses the edge of the machine? There are four boundaries, and three of the four are closed unless you personally open them.

Boundary 1 — Inside the sandbox

Always on

Capture (ScreenCaptureKit + microphone), transcription (Whisper or Parakeet), diarization, embeddings, FTS5 and vector search, and local LLM inference through llama.cpp or MLX. This is the default path end to end. Nothing here touches a socket.

Boundary 2 — Hosted LLM

Opt-in

If you configure a hosted provider and paste a key, prompt text — which means transcript excerpts — leaves the machine for that endpoint. This is the only boundary that carries meeting content in a form the far side can read.

Boundary 3 — iCloud sync

Opt-in

Content crosses as AES-256-GCM ciphertext under a key derived from your seed phrase and never uploaded. Apple stores bytes it cannot read; so can we. Structural metadata crosses in the clear — the specifics are listed below.

Boundary 4 — Local MCP server

Opt-in

A loopback-bound HTTP listener that lets a local agent read your meetings. It never leaves 127.0.0.1, every client is either approved in a consent dialog or holds a token that exists only in memory, and it is off until you switch it on.

Every outbound connection

We enumerated every URLSession and URLRequest construction in the shipping targets. This is all of them. Four go out, one only listens on loopback.

Configured LLM provider — api.openai.com, api.anthropic.com, generativelanguage.googleapis.com, api.deepseek.com, openrouter.ai, or any custom/self-hosted base URL

What is sent
Prompt text. For summarisation, that is transcript excerpts. For chat, it is your question plus whatever the retrieval tools returned — transcript snippets, topics, action items, and the titles and dates of the meetings they came from. Your own hand-written notes are never put in a prompt: no prompt-building path in the app reads the document’s notes field.
When
Only when the LLM provider is set to “OpenAI-Compatible” AND a model is configured AND (for the named providers) an API key has been entered. A missing key throws before any request is built.
Who decides
User-configured. Never the destination when the provider is set to Local Models.
  • noted/Features/LLM/OpenAICompatibleAPI.swift:375, 444, 521, 607
  • noted/Features/LLM/OpenAICompatibleProvider.swift:31–44
  • noted/Features/Summarization/Pipeline/LLMAdapter.swift:335–360

huggingface.co (and its CDN redirect target)

What is sent
HTTP GET / ranged GETs for model weights. No user content, no identifiers beyond what the transport itself carries.
When
User taps download for a local Whisper, Parakeet or GGUF model.
Who decides
User-initiated, per model.
  • noted/Core/Services/HFParallelDownloader.swift:102–110, 230, 329
  • noted/Core/Managers/WhisperModelManager.swift:62, 158, 219
  • noted/Core/Models/LocalModel.swift:162–790 (model catalogue URLs)

itunes.apple.com/lookup

What is sent
The app’s own bundle ID (app.summitnotes). Nothing else — it is a public catalogue lookup for the current App Store version.
When
Automatic: 5 seconds after launch, then every 24 hours while the app runs.
Who decides
Automatic, and there is no setting that suppresses it — we looked, and the check is ungated. This is the one unprompted outbound request the app makes. Blocking itunes.apple.com at the firewall costs you nothing but the new-version banner.
  • noted/Core/Services/UpdateCheckService.swift:30–33, 152–160
  • noted/App/notedApp.swift:150–154

Apple CloudKit — private database, container iCloud.app.summitnotes.container, zone SummitDocuments

What is sent
Per-field AES-256-GCM ciphertext for titles, notes, transcript text, topics and action items; the companion’s audio is uploaded as an encrypted CKAsset. Structural metadata stays in the clear (see below).
When
Only when iCloud Sync is switched on. It is off by default and behind a feature flag that also defaults to off.
Who decides
Opt-in, and unusable without the user’s seed phrase.
  • SummitCore/Sources/SummitCore/Sync/CloudKitSyncService.swift:16–21, 128–148
  • noted/Utilities/Configuration/AppSettings.swift:1217
  • noted/Utilities/Configuration/FeatureFlags.swift:19–20

127.0.0.1 (inbound, loopback only)

What is sent
Answers to MCP tool calls from a local client: meeting lists, search results, transcripts, topics, action items.
When
Only when the local MCP server is enabled. Off by default. Every request needs either an OAuth grant the user approved in a consent dialog, or a bearer token that lives only in memory.
Who decides
Opt-in. Loopback-bound; never exposed off-device.
  • noted/Core/Services/MCPServerManager.swift:8–21, 441–470
  • noted/Utilities/Configuration/AppSettings.swift:736

Just as important: what is absent

There is no analytics SDK, no crash reporter, no PostHog, Mixpanel, Sentry, Firebase or Amplitude anywhere in the app targets. There is no Summit-operated backend at all — we run no server that your app talks to, which is why no amount of compromise on our side reaches your recordings. Purchases are verified on-device with StoreKit 2 signed transactions, so even billing needs no server of ours. The AnalyticsService you will find in the source keeps at most 100 events in memory and flushes them to the macOS unified log — the code path that would send them somewhere does not exist.

Where the data actually sits

Data Location At rest Leaves the device?
Audio recordings (.m4a) Sandboxed container, or a folder the user picks macOS file permissions + sandbox + FileVault if enabled. Not separately encrypted by the app on disk. Only as an encrypted CKAsset, and only from the iOS companion when sync is on.
Transcripts, summaries, topics, action items SQLite (GRDB) in Application Support Plain SQLite in WAL mode. No SQLCipher passphrase — protection is the OS container, not app-level encryption. Only as AES-256-GCM ciphertext via CloudKit, or as prompt text if a cloud LLM is configured.
Search index (vectors + FTS5) VecturaKit store in Application Support; FTS5 tables in the same SQLite file Same as the database. Embeddings are computed on-device. No. The index is never synced or uploaded.
Model weights (Whisper, Parakeet, GGUF) Application Support / Models Public artefacts, no user content. No.
LLM provider API keys Keychain, generic password, service app.summitnotes.llm-provider-credentials kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — does not sync to iCloud, does not leave the Mac. Only as an Authorization / x-api-key header to the endpoint the user configured.
Sync encryption key Keychain, service app.summitnotes.encryption, kSecAttrSynchronizable = false AES-256 key derived from a 12-word BIP-39 phrase the user holds. Never to CloudKit and never to us — but pairing moves it. Re-typing the phrase on the phone carries it out of band; the QR option puts the raw key on screen for the camera. Treat both as handling the password to the whole library.
Usage analytics In-memory queue, max 100 events, flushed to the unified log Never persisted by the app; never transmitted. No. There is no analytics backend in the client.
Calendar & Reminders EventKit, read for meetings; written only when the user exports an action item Owned by the OS, not copied wholesale into the app. Summit contacts no calendar provider itself. But an action item you export lands in EventKit, and if the target list is backed by iCloud, Google or Exchange, that account syncs the text off the Mac — the OS does it, not us. A local-only list stays local.

The local path, step by step

Once you have downloaded a local model and set the provider to Local Models, a recording goes from microphone to finished summary without a single byte crossing the sandbox. Be precise about what that means: a fresh install ships with no model on disk and no API key, so step 4 cannot run until you choose one or the other. Downloading a model is a one-off fetch from Hugging Face; entering a provider key is what opens the hosted path. Each step below names the code that performs it.

  1. 1

    Capture

    ScreenCaptureKit takes system audio, AVFoundation takes the microphone, and the two streams are mixed locally. Screen recording permission is requested for the sole purpose of capturing the other side of the call — the usage string says exactly that.

    noted/Features/Recording/ScreenAudioRecorder.swift:140, 219, 245 · noted/Info.plist:5

  2. 2

    Transcribe

    whisper.cpp or FluidAudio/Parakeet runs in-process against a model file on disk. Both engines are initialised from a local path; there is no speech API call.

    noted/Features/Transcription/WhisperTranscriber.swift:45–62 · FluidAudioAsrManager.swift:245

  3. 3

    Diarize & clean

    Speaker separation, echo de-duplication and silence trimming, all on-device.

    noted/Features/Transcription/DiarizationService.swift:13–23

  4. 4

    Summarize

    The pipeline chunks the transcript and calls the configured LLM. With Local Models that is llama.cpp or MLX loading a GGUF from Application Support — same machine, same process.

    noted/Features/LLM/LlamaService.swift:103–160 · Summarization/Pipeline/SummarizationPipeline.swift:58–90

  5. 5

    Index

    Embeddings are generated on-device and written to a VecturaKit store next to the models; keyword search uses SQLite FTS5 in the same database.

    noted/Features/Search/SearchManager.swift:169–184

  6. 6

    Store

    Everything lands in one SQLite file in Application Support, with audio alongside it or in a folder you nominate.

    noted/Core/Database/DatabaseManager.swift:91–105

Keys and secrets

Sync encryption key

A 12-word BIP-39 phrase generated on the Mac, from which both devices derive the same AES-256 key. The derived key is cached in the local Keychain with synchronizable = false, so it never rides iCloud Keychain to anywhere. We hold no copy and no escrow. Lose the phrase and the ciphertext in CloudKit is unrecoverable — that is the trade we chose.

SummitCore/.../CloudKitEncryption.swift:147–174, 245–296

Provider API keys

Stored as Keychain generic passwords under a fixed service name, accessible only after first unlock and marked this-device-only. They are never written to UserDefaults and never leave the Mac except as an auth header to the endpoint you configured.

noted/Utilities/Configuration/CredentialStore.swift:26–72

MCP bearer token

256 bits from SecRandomCopyBytes, held in memory for the process lifetime and deliberately never persisted — a long-lived server would otherwise need an at-rest secret it could read without authentication. The cost is that you re-copy the token after a restart.

noted/Core/Services/MCPServerManager.swift:441–475

Sandbox posture

Read off the signed target’s build settings and entitlements. The denials matter as much as the grants: an app that cannot reach your camera, contacts or location cannot leak them however it is compromised. Every row comes from one of two files — the build settings in noted.xcodeproj/project.pbxproj:1250–1290 (and the matching Release block at 1322–1362), or the signed entitlements in noted/noted.entitlements:1–27.

Capability State Source
App Sandbox Enabled ENABLE_APP_SANDBOX = YES
Outgoing network Allowed Required for the five egress paths above
Incoming network Granted via entitlement com.apple.security.network.server, used by the loopback MCP listener
Microphone Allowed ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = YES
Calendars Allowed com.apple.security.personal-information.calendars
Camera Denied ENABLE_RESOURCE_ACCESS_CAMERA = NO
Contacts Denied ENABLE_RESOURCE_ACCESS_CONTACTS = NO
Location Denied ENABLE_RESOURCE_ACCESS_LOCATION = NO
Bluetooth Denied ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO
USB Denied ENABLE_RESOURCE_ACCESS_USB = NO
Printing Denied ENABLE_RESOURCE_ACCESS_PRINTING = NO
User-selected files Read/write For the recordings folder and Markdown export target
App Transport Security Default No NSAllowsArbitraryLoads or other ATS exception anywhere in the project

Defaults as shipped

A privacy claim is only worth what the defaults are worth. Here is what a fresh install does before you change anything.

Setting Default Source
iCloud sync Off AppSettings.swift:1217 · FeatureFlags.swift:19–20
Local MCP server Off AppSettings.swift:736
Markdown auto-export Off AppSettings.swift:807
Local LLM engine llama.cpp (on-device) AppSettings.swift:394
LLM provider OpenAI-Compatible, pre-selected as OpenAI — but inert until the user supplies a key and model AppSettings.swift:346, 353 · LLMAdapter.swift:347–353
Analytics On, but local-only — the flush path writes to the unified log and nowhere else AnalyticsService.swift:39, 275–289

Limitations and open findings

A security page that lists only strengths is marketing. These are the things we would raise if we were reviewing someone else’s code, so we raise them about our own.

Local database and audio are not encrypted by the app

By design, worth knowing

The SQLite store is opened through GRDB with foreign keys, WAL and cache pragmas — there is no SQLCipher key. Recordings sit on disk as ordinary .m4a files. At rest, the protections are the sandbox container, macOS file permissions and FileVault. On a Mac without FileVault, an attacker with the disk can read both.

  • noted/Core/Database/DatabaseManager.swift:91–105

Downloaded model weights are not checksum-verified

Supply-chain gap

The catalogue carries a checksum field and the download manager will verify it when present — but every entry in the shipping catalogue sets it to nil, so verification never runs. Integrity currently rests on TLS plus the fact that 12 of the 34 model URLs are pinned to an immutable Hugging Face revision; the remaining 22 resolve against a mutable /main.

  • noted/Core/Models/LocalModel.swift (all entries: checksum: nil)
  • noted/Core/Managers/LocalModelManager.swift:136–137

Sync metadata is deliberately not encrypted

By design, disclosed

Field-level encryption covers titles, notes, transcript text, original text, topics and action-item content. Left in the clear so CloudKit can query and reconcile them: record IDs, created/modified timestamps, recording duration, word count, detected language, folder and template IDs, document state, and which device last touched the record. Someone with access to the CloudKit zone learns when you met, for how long, in what language and how much was said — but not a word of what was said.

  • SummitCore/Sources/SummitCore/Sync/CloudKitRecordConversion.swift:33–46, 109–119

A stale comment overstates key syncing

Documentation drift

The header comment in CloudKitRecordConversion.swift describes the encryption key as living in the iCloud Keychain with kSecAttrSynchronizable: true. The implementation does the opposite: the key is stored strictly locally with synchronizable set to false, and cross-device pairing is manual — a 12-word phrase or a QR transfer. The code is the stricter of the two; the comment is wrong and should be corrected.

  • SummitCore/Sources/SummitCore/Sync/CloudKitEncryption.swift:250, 289
  • SummitCore/Sources/SummitCore/Sync/CloudKitRecordConversion.swift:6–8

QR key transfer moves raw key material

Handle like a password

exportKeyTransferPayload() encodes the raw 32-byte AES key into a payload rendered as a QR code for pairing a phone. Anyone who photographs that screen can decrypt the whole synced library. The code says as much in its own doc comment; the mitigation is procedural, not technical.

  • SummitCore/Sources/SummitCore/Sync/CloudKitEncryption.swift:191–215

Entitlement and build setting disagree on inbound connections

Configuration hygiene

The target sets ENABLE_INCOMING_NETWORK_CONNECTIONS = NO while the entitlements file grants com.apple.security.network.server. The explicit file is what gets signed, and the loopback MCP listener needs it — but the two should be reconciled so the build settings tell the truth about the shipped posture.

  • noted/noted.entitlements:12–19
  • noted.xcodeproj/project.pbxproj:1264, 1336

Cloud LLM usage is a user decision the app cannot undo

Understand before enabling

When the provider is set to a hosted endpoint, transcript text is sent to that endpoint in the clear (inside TLS) and is thereafter governed by that provider’s retention and training policy, not by Summit. There is no redaction pass, no PII filter and no local proxy in front of it. Choosing Local Models — or pointing the OpenAI-compatible provider at Ollama or LM Studio on localhost — keeps everything on the machine.

  • noted/Features/LLM/OpenAICompatibleAPI.swift:444–470

Check our work

You do not need our source to test the central claim. With Summit running on stock settings, put it behind a proxy or watch it with Little Snitch and record a meeting end to end. You should see the App Store version lookup and nothing else — no transcript, no audio, no telemetry.

With source access, these commands cover the direct HTTP half of the egress table — the paths the app builds itself:

# every call site that builds an HTTP request itself
grep -rn "URLSession\|URLRequest(" --include=*.swift .
# every hard-coded host
grep -rnoE "https?://[a-zA-Z0-9._/-]+" --include=*.swift noted SummitCore SummitCompanion | sort -u
# confirm no analytics or crash-reporting SDK is linked
grep -rni "posthog\|mixpanel\|sentry\|firebase\|amplitude\|crashlytics" --include=*.swift .
# the rest of the network surface: Apple frameworks and the dependency graph
grep -rn "CKDatabase\|CKContainer\|NWConnection\|NWListener" --include=*.swift . && cat SummitCore/Package.resolved

Grepping for URLSession alone would not find everything, and we would rather say so than let the recipe imply otherwise. CloudKit talks to Apple through CKDatabase, the MCP listener uses NWListener, EventKit and StoreKit reach Apple’s services without the app opening a socket at all, and a linked dependency could in principle carry its own client — which is what the last command and the resolved package list are for. The packet capture above is the check that does not depend on knowing which API was used.

If any of these turn up something this page does not account for, the page is wrong and we want the report.

Scope of this review

This is a first-party architecture review by the Summit team, not a third-party penetration test or a certification. It covers the macOS app, the iOS companion and the shared SummitCore package at commit 9b419e0. It reads the source as written; it does not prove the absence of memory-safety bugs, and it makes no claim about the security of third-party model weights, of the LLM providers you may choose to configure, or of Apple’s CloudKit and Keychain implementations beyond the guarantees Apple documents.

Development tooling in the repository — the evaluation CLI, which does call an Anthropic endpoint — is excluded: it is not part of any shipped target and never runs on a user’s machine.

Found something we missed?

Security reports, architecture questions and requests for source access under NDA all go to the same place. We answer them.