# Settings reference

> All persisted SettingsStore keys: UserDefaults fields (appearance, hotkey, dictationMode, notch position, service-issue flags), Keychain accounts (cartesiaAPIKey, llm.{provider}), SMAppService launchAtLogin, and derived properties transcriptionIssue / polishIssue.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `InkIt/SettingsStore.swift`
- `InkIt/LLMProvider.swift`
- `InkIt/DebugLog.swift`
- `InkIt/SettingsView.swift`
- `DESIGN_SYSTEM.md`

---

---
title: Settings reference
description: All persisted SettingsStore keys — UserDefaults fields, Keychain accounts, SMAppService launch-at-login, and derived service-issue properties.
slug: settings-reference
---

`SettingsStore` is InkIt's single source of truth for user preferences and API credentials. It is a `final class` singleton (`SettingsStore.shared`) exposed as an `@EnvironmentObject` throughout the app. Properties are `@Published` so SwiftUI views react to changes; each write persists immediately to the appropriate backing store.

## Storage architecture

InkIt splits settings across three backends by sensitivity and system integration:

```mermaid
flowchart LR
  SS[SettingsStore]
  UD[UserDefaults.standard]
  KC[Keychain generic passwords]
  SM[SMAppService.mainApp]
  SS --> UD
  SS --> KC
  SS --> SM
```

| Backend | What lives here | Why |
| --- | --- | --- |
| **UserDefaults** | Non-secret preferences, hotkey encoding, service-issue flags | Fast reads; plist at `~/Library/Preferences/ai.cartesia.InkIt.plist` |
| **Keychain** | `cartesiaAPIKey` and per-provider LLM keys | Secrets never touch plaintext UserDefaults on signed builds |
| **SMAppService** | Launch-at-login registration | System Login Items is the source of truth — no separate persisted flag |

API keys use the Keychain **service** `Bundle.main.bundleIdentifier` (`ai.cartesia.InkIt`) with **account** names documented below. On ad-hoc or unsigned local builds, `Keychain.usesKeychain` is `false` and secrets fall back to UserDefaults keys prefixed `secretFallback.` (for example `secretFallback.cartesiaAPIKey`).

## UserDefaults keys

All keys below are written to `UserDefaults.standard` unless noted. Boolean keys default to `false` when absent unless a custom default is listed.

### Appearance and app chrome

<ParamField body="appearancePreference" type="string">
One of `system`, `light`, or `dark`. Maps to `AppearancePreference`. Applied app-wide via `NSApp.appearance` on change and at launch. Default on first run: `light`. The always-dark notch HUD ignores this setting.
</ParamField>

<ParamField body="hasCompletedOnboarding" type="bool">
Whether the user finished the onboarding flow (permissions, Cartesia key, Try It). Default: `false`. Gates notch HUD visibility until complete.
</ParamField>

### Dictation behavior

<ParamField body="dictationMode" type="string">
`hold` (press-and-hold, release to paste) or `toggle` (hands-free: press to start, press again to paste). Maps to `DictationMode`. Default: `hold`. Read by `AppCoordinator` hotkey handlers.
</ParamField>

<ParamField body="hotkeyKind" type="string">
Discriminator for the global dictation shortcut: `fn`, `carbon`, or `modifier`. Written by `saveHotkey()` whenever `hotkey` changes.
</ParamField>

<ParamField body="hotkeyKeyCode" type="int">
Carbon virtual key code (`UInt32` stored as `Int`). Used when `hotkeyKind` is `carbon` or `modifier`. For `carbon`, defaults to Space if missing; for `modifier`, defaults to Control.
</ParamField>

<ParamField body="hotkeyModifiers" type="int">
Carbon modifier mask (`UInt32` stored as `Int`). Only used when `hotkeyKind` is `carbon`. Default combo if missing: Control + Option + Space.
</ParamField>

<ParamField body="playFeedbackSounds" type="bool">
Whether InkIt plays audio cues on hotkey press and release. Default: `true` when the key is absent.
</ParamField>

<ParamField body="preferredInputDeviceUID" type="string">
CoreAudio UID of the pinned microphone, or `""` to follow the system default. Passed to `AudioCaptureService` at record time; stale UIDs fall back safely when the device is unplugged.
</ParamField>

### Polish (LLM rewrite)

<ParamField body="correctionEnabled" type="bool">
Master toggle for transcript polish. When `false`, raw STT output is pasted. Default: `false`. Pausing via `pausePolish()` keeps the key on file.
</ParamField>

<ParamField body="rewriteProvider" type="string">
Selected `LLMProvider` raw value: `groq`, `gemini`, `openai`, or `anthropic`. Default: `groq`. Changing provider clears `polishKeyInvalid` and `polishOutOfCredits`.
</ParamField>

<ParamField body="rewriteModel" type="string">
Model ID for the active provider. Default: the provider's `defaultModel` (for example `llama-3.3-70b-versatile` for Groq). Auto-corrected at init if invalid for the selected provider.
</ParamField>

<ParamField body="polishNudgeDismissed" type="bool">
Whether the Home "Polish your dictation" nudge was dismissed. Sticky across launches. The nudge only shows when polish is off.
</ParamField>

### Notch HUD

<ParamField body="notchHorizontalPosition" type="double">
Normalized horizontal position of the notch HUD on the active screen, `0.0` (left) to `1.0` (right). Clamped to `[0.04, 0.96]` on write. Default: `0.38` (slightly left of center). Persisted for future positioning control; the current `NotchHUDController` anchors to detected notch geometry rather than reading this value.
</ParamField>

### Service-issue flags

These booleans persist account problems that drive Home status UI until cleared by a successful operation. Transient failures (offline, 5xx, rate limit) are **not** persisted — they surface only in the notch HUD and history log.

<ParamField body="cartesiaKeyInvalid" type="bool">
Set when STT fails with an invalid or expired Cartesia key (401/403). Cleared on the next successful transcription. Drives the "Dictation is paused — invalid key" Home banner via `transcriptionIssue`.
</ParamField>

<ParamField body="cartesiaOutOfCredits" type="bool">
Set when STT fails for billing reasons (402 / quota exceeded). Cleared on the next successful transcription.
</ParamField>

<ParamField body="polishKeyInvalid" type="bool">
Set when a polish rewrite fails with 401/403. Cleared on successful rewrite, provider change, or `enablePolish()`. Drives Polish settings `keyBroken` state and `polishIssue`.
</ParamField>

<ParamField body="polishOutOfCredits" type="bool">
Set when polish fails for billing (402). Cleared on successful rewrite or provider change.
</ParamField>

### Advanced

<ParamField body="debugLoggingEnabled" type="bool">
Enables developer trace logging to `~/Library/Logs/InkIt-debug.log` and unified logging. Default: `false`. Shared key constant: `DebugLog.isEnabledKey`. Traces include raw transcripts and on-screen context — opt in only while debugging.
</ParamField>

### Legacy keys (migrated, then scrubbed)

On first launch after upgrade, `SettingsStore` migrates these plaintext UserDefaults entries into Keychain and removes them:

| Legacy key | Migrated to |
| --- | --- |
| `cartesiaAPIKey` | Keychain account `cartesiaAPIKey` |
| `llmAPIKeys` (dictionary) | Keychain accounts `llm.{provider}` |
| `anthropicAPIKey` | Keychain account `llm.anthropic` |

## Hotkey binding encoding

The in-memory `hotkey` property is a `HotkeyBinding` enum. Persistence splits it across `hotkeyKind`, `hotkeyKeyCode`, and `hotkeyModifiers`:

| `hotkeyKind` | In-memory variant | Persisted fields | Default |
| --- | --- | --- | --- |
| `fn` | `.fn` | kind only | **Yes** — Fn/Globe via `CGEventTap` |
| `carbon` | `.carbon(keyCode, modifiers)` | kind + keyCode + modifiers | Control+Option+Space if stored combo is invalid |
| `modifier` | `.modifierKey(keyCode)` | kind + keyCode | Left Control |

Carbon combos run `isValidShortcut` checks (reserved system shortcuts like ⌘C, ⌘Space, screenshot keys). Invalid stored combos fall back to `.fn` at init.

## Keychain accounts

Secrets are generic-password items. Empty strings remove the item.

<ResponseField name="cartesiaAPIKey" type="string">
Cartesia STT API key. Bound to `SettingsStore.cartesiaAPIKey`. Used by `CartesiaStreamingClient` for WebSocket auth.
</ResponseField>

<ResponseField name="llm.groq" type="string">
Groq API key for polish when `rewriteProvider` is `groq`.
</ResponseField>

<ResponseField name="llm.gemini" type="string">
Google Gemini API key.
</ResponseField>

<ResponseField name="llm.openai" type="string">
OpenAI API key.
</ResponseField>

<ResponseField name="llm.anthropic" type="string">
Anthropic API key.
</ResponseField>

The in-memory `llmAPIKeys` dictionary is keyed by `LLMProvider.rawValue`. Use `apiKey(for:)` and `setAPIKey(_:for:)` for per-provider access. Writing any entry syncs all provider slots to Keychain (empty values delete their items).

### Keychain persistence across rebuilds

Keychain items are bound to the app's code signature. **Signed release builds** (Developer ID, not ad-hoc) retain keys across updates. **Ad-hoc "Sign to Run Locally"** Debug builds re-sign each compile, which orphans Keychain items — those builds use the `secretFallback.*` UserDefaults path instead.

## Launch at login (`SMAppService`)

<ResponseField name="launchAtLogin" type="bool">
Mirrors `SMAppService.mainApp.status == .enabled`. **Not stored in UserDefaults.** Toggling registers or unregisters the login item; `syncLaunchAtLoginFromSystem()` reconciles when Settings opens (the user may change Login Items in System Settings). On registration failure, the toggle reverts to the actual system state.
</ResponseField>

## Derived properties (not persisted)

These are computed from persisted state and drive UI logic. They are not written to disk.

### `transcriptionIssue` and `polishIssue`

```mermaid
flowchart TD
  subgraph transcription
    CK[cartesiaAPIKey non-empty?]
    KI[cartesiaKeyInvalid]
    OC[cartesiaOutOfCredits]
    CK -->|no| TN[nil]
    CK -->|yes| KI
    KI -->|true| TKI[keyInvalid]
    KI -->|false| OC
    OC -->|true| TOC[outOfCredits]
    OC -->|false| TH[nil healthy]
  end
  subgraph polish
    CE[correctionEnabled + hasRewriteKey]
    PK[polishKeyInvalid]
    PO[polishOutOfCredits]
    CE -->|no| PN[nil]
    CE -->|yes| PK
    PK -->|true| PKI[keyInvalid]
    PK -->|false| PO
    PO -->|true| POC[outOfCredits]
    PO -->|false| PH[nil healthy]
  end
```

Both return `SettingsStore.ServiceIssue?` — either `.keyInvalid` or `.outOfCredits`, or `nil` when healthy.

| Property | Suppressed when | Home UI |
| --- | --- | --- |
| `transcriptionIssue` | No Cartesia key set (onboarding, not a fault) | Full-width **Dictation is paused** banner |
| `polishIssue` | Polish off or no rewrite key | Calm **Polish is paused** rail card |

**Setters:** `AppCoordinator.handleSTTFailure` sets Cartesia flags for `.invalidKey` / `.outOfCredits`. Polish flags are set in the `onClosed` handler when `RewriteFailure` maps to `.invalidKey` or `.outOfCredits`.

**Clearers:** Cartesia flags clear on any successful STT transcript. Polish flags clear on `.polished` outcome, provider change, or `enablePolish()`.

### Other derived helpers

| Property | Purpose |
| --- | --- |
| `hasRewriteKey` | Whether the active `rewriteProvider` has a non-empty Keychain key |
| `polishUIState` | Polish settings pane state: `setup`, `on`, `paused`, or `keyBroken` |
| `hotkeyDisplayString` | Human-readable shortcut for UI (for example `🌐 fn`, `⌃⌥Space`) |
| `dictationModeVerb` | `"Press"` for toggle mode, `"Hold"` for hold mode — single source for gesture copy |

### `PolishUIState` truth table

| State | Condition |
| --- | --- |
| `setup` | No rewrite key for the selected provider |
| `paused` | Key exists but `correctionEnabled == false` |
| `keyBroken` | Key exists, polish enabled, `polishKeyInvalid == true` |
| `on` | Key exists, polish enabled, key healthy |

## Settings UI mapping

Settings is organized into three panes. Each persisted property maps to a control:

| Pane | Property | Control |
| --- | --- | --- |
| **General** | `appearance` | Appearance card picker |
| **General** | `launchAtLogin` | Launch at login toggle |
| **General** | `debugLoggingEnabled` | Debug logging toggle |
| **Dictation** | `dictationMode` | Activation mode cards |
| **Dictation** | `hotkey` | Hotkey recorder |
| **Dictation** | `playFeedbackSounds` | Sound on press/release toggle |
| **Dictation** | `preferredInputDeviceUID` | Microphone picker |
| **Dictation** | `cartesiaAPIKey` | Cartesia API key field |
| **Polish** | `correctionEnabled` | Polish transcripts toggle |
| **Polish** | `rewriteProvider` | Provider picker |
| **Polish** | `rewriteModel` | Read-only model row |
| **Polish** | `llmAPIKeys[provider]` | Per-provider API key field |

Permissions (microphone, accessibility) are **not** stored in `SettingsStore` — they are runtime OS grants polled by `PermissionsService`.

## Programmatic access

`AppCoordinator` holds `let settings = SettingsStore.shared`. Any feature that needs preferences should read from this singleton rather than `UserDefaults` directly, so migrations and Keychain routing stay centralized.

```swift
// Read the active polish key
let key = SettingsStore.shared.apiKey(for: .groq)

// Check whether Home should show a service card
if let issue = SettingsStore.shared.transcriptionIssue { /* … */ }
```

## Related pages

<Card href="/configure-cartesia-api-key" title="Configure Cartesia API key" icon="key">
Keychain storage, validation, and how `cartesiaKeyInvalid` / `cartesiaOutOfCredits` drive Home cards.
</Card>

<Card href="/configure-polish" title="Configure Polish" icon="wand-and-stars">
Provider selection, per-provider Keychain keys, `correctionEnabled`, and `PolishUIState`.
</Card>

<Card href="/configure-hotkeys-and-mode" title="Configure hotkeys and dictation mode" icon="keyboard">
Hotkey recording, `DictationMode`, `playFeedbackSounds`, and `notchHorizontalPosition`.
</Card>

<Card href="/hotkey-bindings" title="Hotkey bindings" icon="command">
`HotkeyBinding` variants, Carbon vs Fn vs bare modifier, and `isValidShortcut` rules.
</Card>

<Card href="/input-device-selection" title="Input device selection" icon="mic">
`preferredInputDeviceUID` pinning, fallback behavior, and Bluetooth caveats.
</Card>

<Card href="/stt-troubleshooting" title="STT troubleshooting" icon="exclamationmark-triangle">
When Cartesia failures set persisted flags vs transient notch errors.
</Card>

<Card href="/polish-troubleshooting" title="Polish troubleshooting" icon="exclamationmark-triangle">
`polishKeyInvalid`, `polishOutOfCredits`, and raw-transcript fallback behavior.
</Card>

<Card href="/runtime-troubleshooting" title="Runtime troubleshooting" icon="wrench">
Debug logging, Keychain ad-hoc fallback, and notch HUD positioning.
</Card>
