shared/build.gradle.kts, SKIE enabled for Swift interopSteelNotes.sq — full schema with triggers and named queriesFrontmatterParser.kt — hand-rolled, no external YAML depAnnotationParser.kt[[links]] → markdown links on save)▶[[ ]] removed; see NoteLinkExtractor.kt and LINKING_ARCHITECTURE.md)*Note.kt, NoteType.kt, Statuses.kt, Annotation.kt, Connection.kt. See Phase 6 for Note model refactor.NoteFactory.ktNoteSerializer.ktsrc-2026-03-20-001 pattern)▶IdGenerator.ktFileSystem.kt (expect) + PlatformFileSystem.kt (appleMain/NSFileManager)VaultManager.kt — init, create, update, delete, list, wikilink indexingVaultIndexer.kt — fullReindex, indexFile, deindexFileSearchEngine.kt — search, findRelated, filtersDatabaseDriverFactory.kt (expect + appleMain NativeSqliteDriver)cli/ — all 7 commands, --json flag, Clikt frameworkiosApp/project.yml (XcodeGen), generates SteelNotes.xcodeprojSteelNotesApp.swift entry point▶iosApp/SteelNotes/App/SteelNotesApp.swiftembedAndSignAppleFrameworkForXcode Gradle task in pre-build scriptSharedCoreService.swift (Swift bridge to Kotlin)▶iosApp/SteelNotes/Services/SharedCoreService.swift — facade over VaultManager, SearchEngine, VaultIndexeriosApp/SteelNotes/DI/AppContainer.swift — ObservableObject with vault path resolutioniosApp/SteelNotes/Navigation/AppNavigation.swift — compact drawer navigation, deep links, programmatic drawer open/close animation, and smoother interactive drag-open settlingGoal: Get .md files syncing across devices via S3 + custom REST API. This is the highest-priority feature — ship sync before rich iOS editing.
See SYNC_ARCHITECTURE.md for the full sync design spec.
See AUTH_ARCHITECTURE.md for the multi-provider auth design.
See SYNC_BUILD_GUIDE.md for the actionable build plan.
server/ Rust project: Cargo.toml, Axum router, config, error handling▶server/src/main.rs — Axum 0.8, public/protected route groups, Lambda + standalone entryusers, auth_providers, refresh_tokens, devices, file_state▶004_auth_providers_refresh_tokens.sql — migrates provider data, adds auth_providers + refresh_tokensPOST /auth/register — email/password sign-up (Argon2id)▶server/src/routes/auth.rs — Argon2id hashing, creates user + auth_provider + devicePOST /auth/login — unified endpoint with grant_type discriminator▶#[serde(tag = "grant_type")] enumPOST /auth/refresh — refresh token rotation▶POST /auth/link — link additional provider to existing account▶POST /devices/register — device + push token registration▶server/src/routes/devices.rs — upsert by user_id + device_nameserver/src/middleware/auth.rs — extracts user_id + device_id into extensionsserver/src/services/s3.rs — presigned PUT/GET/DELETEserver/deploy/terraform/main.tf — full infra definitionAuthManager.kt, TokenStore.kt (expect/actual for Keychain)▶com.steelnotes.auth — AuthManager + TokenStore (Keychain stub, in-memory for now)SyncClient.kt skeleton (Ktor HTTP wrapper)▶com.steelnotes.sync.SyncClient — all endpoints + S3 direct upload/downloadsteel auth register/login/status/logout commands▶cli/Main.kt — AuthCommand with register/login/status/logout subcommands, tokens in ~/.steelnotes/auth.json (mode 600)SignInView.swift with Sign in with Apple▶iosApp/Views/Auth/SignInView.swift — Sign in with Apple + skip optionGET /vault/state — full and incremental (?since=)▶server/src/routes/vault.rs — filters by updated_at, separates active/deletedPOST /vault/push — version check + presigned upload URLs▶conflicts[] with download URLPOST /vault/push/confirm — update file_state + trigger push notifications▶POST /vault/pull — presigned download URLs▶POST /vault/delete — version-checked deletion with tombstones▶is_deleted=true, deletes from S3, notifies devicesSyncStateDatabase.sq (.steel/sync.db schema)▶SyncState.sq — sync_state, sync_meta, sync_log tables + full query setChangeDetector.kt — SHA-256 hash-based local file diffing▶Sha256.kt (expect/actual, CommonCrypto on Apple) + ChangeDetector.ktSyncManager.kt — full sync cycle (5 phases)▶server/src/services/push.rs — SNS fanout to APNs with silent push payloadcontent-available: 1syncNow()▶AppDelegate.swift handles silent push → posts .vaultChangedRemotely → SyncService.syncNow()SyncStatusView — toolbar sync indicator▶SyncStatusView.swift — toolbar icon (syncing/synced/offline/error/conflict) + SyncDetailViewSyncManager.onLocalFileChanged() — 2s debounce via coroutine JobDesign choice: Conflicts resolve automatically — no manual diff UI. Last-write-wins for overlapping edits, auto-merge for non-overlapping. Users should never see a conflict screen.
ConflictResolver.kt — frontmatter + body split merge▶ConflictResolver.kt — splits YAML frontmatter from body, merges independentlythreeWayMergeLines()ConflictResolver returns AutoMerged with remote (newer) version when 3-way merge fails; SyncManager.processConflicts() always auto-resolvesSyncStateStore.log("auto_merge", ...) and log("last_write_wins", ...) entries for all conflict resolutionsDeferred — not priority until auth, sync, and capture are solid.
GET /vault/export — zip download of entire vault▶SyncSettingsView.swift — account info, storage, device list▶SyncSettingsView.swift — account status, sync detail, sign outSyncDetailView.swift — status dot, sync now button, filterable log list (push/pull/merge/delete/conflict events), relative timestamps, path + detail displayAppError enum covers auth/quota/rate-limit/network; SyncManager catches exceptions with offline fallbackSyncManager marks files PENDING_PUSH immediately, syncs on next syncNow()POST /maintenance/cleanup — deletes tombstones >90 days + expired refresh tokensGoal: Fastest possible capture, with background AI that classifies, structures, and links notes automatically. AI is additive — original capture always preserved.
See CAPTURE_ARCHITECTURE.md for the full design spec.
steel add command (URLs, text, files)▶inbox/capture-{timestamp}.md with minimal frontmatter)▶POST /vault/push/confirm for inbox/ pathsaws-sdk-bedrockruntime + aws-sdk-transcribe to Cargo.toml▶server/src/services/bedrock.rs — Bedrock client wrapper▶InvokeModel for text, vision via Claude on Bedrock. Reuse AWS config.server/src/services/transcribe.rs — Amazon Transcribe wrapper▶StartTranscriptionJob reads audio from S3, polls for completionbedrock:InvokeModel + transcribe:Start/GetTranscriptionJobresizeAspectFill using preview-layer metadata rect conversion, mapped live Vision orientation from AVCaptureConnection to CGImagePropertyOrientation (including corrected direction to avoid inverted X/Y tracking), normalized batch image orientation, aligned corners frame-to-frame before temporal tracking to preserve perspective direction under tilt, added adaptive candidate smoothing to reduce random box jumps without lagging behind motion, merged vertically aligned split Vision rectangles (top/bottom segments) into a single cover candidate, uses a larger internal Vision observation pool so merge logic still works even when user-facing max detections is set low, and now shows secondary detections in live debug while boosting merged-candidate ranking by area gain/ai/analyze and the async worker; local env docs include GEMINI_API_KEY; deployment must use a freshly regenerated target/lambda/<bin>/bootstrap.zip because cargo lambda build can leave that zip stale even when the Rust binary changed, and Terraform's placeholder.zip is not the real deployment artifact.noteId, which points at the existing resource instead of the capture bundleinbox/ to resources/ or thoughts/)▶processing_status indicator in vault list▶getQuestionsToResurface()▶GraphEngine.ktGoal: Full-featured iOS reading and editing experience. Delayed intentionally — sync and capture are higher value than on-phone editing right now.
Theme/Theme.swift — programmatic adaptive colors via UIColor trait collection, follows system preferenceStatusBadge and TagsView components▶Views/Components/ — shared across vault, detail, searchNoteType theme extensions (icon, color, display names)▶Theme.swift — iconName, color, displayName, singularNameSteelNotesApp.swift + AppNavigation.swift — .tint(._steelAccent)CaptureView.swift — camera-first capture▶CaptureView.swift record button now fires immediate touch-down impact and keeps release feedbackSteelNotesShare/ target)▶SteelNotesShare/ — accepts text, URLs, images via App Groupgroup.com.steelnotes.shared — entitlements + UserDefaults + auto-import on launchSteelNotesWidget/ target)▶SteelNotesWidget/ — QuickCaptureWidget + RecentNotesWidgetsteelnotes://capture, shows last note titlesteelnotes:// URL scheme — capture, note/{id}, search, vault routesVaultView.swift — async loading, pull-to-refresh▶.refreshable, type chip picker, + button, themed backgroundsNoteRow shows title, status badge, tags, author (for resources), relative dateNoteRow with type-aware status colorsNavigationLink(value:) + navigationDestination(for:)NoteDetailView.swift — note reader with metadata▶MarkdownBodyView.swift — markdown renderer▶AnnotationCard — type icon, highlighted text, note, color-coded by typefindRelated() FTS5 query, shows type icon + title linksEditorView.swift — create/edit all note types▶[[ trigger)SearchView.swift — search with debounced FTS5▶.searchable modifier, 300ms debounce, result rows with snippet + scoreSearchResultRow — type icon, title, snippet, match percentageGraphView.swift)Goal: Bring Steel Notes to Android using Jetpack Compose for UI and the existing KMP shared module for all business logic. Same architecture as iOS — UI layer only, all logic in Kotlin shared.
androidApp/ — Gradle module, Material 3, min SDK 26shared as Gradle dependency, no framework bridging needed (Kotlin native)AuthManager.kt from shared moduleVaultScreen — note list with type filters▶NoteDetailScreen — reader with transclusion rendering▶ inline as embedded cardsSearchScreen — FTS5 search with debounce▶SearchEngine.kt from shared moduleEditorScreen — create/edit all vault item types▶CaptureScreen — camera-first capture▶SyncManager.kt▶syncNow(), replaces APNs pathTokenStore actual for Android (EncryptedSharedPreferences)▶PlatformFileSystem actual for Android▶Context.filesDir based file I/ODatabaseDriverFactory actual for Android▶Sha256 actual for Android▶java.security.MessageDigestRichEditor composable — TextField with AnnotatedString▶AnnotatedString ↔ markdown converter▶ from body▶!\[\[([^\]]+)]], same code-block exclusion as WikilinkExtractorWikilinkExtractor regex to skip ![[ via negative lookbehind▶(?<!!)\[\[([^\]]+)]]resource_refs TEXT column to notes table▶SteelNotes.sq — nullable, comma-separated Resource IDsinsertNote query with resource_refs parameterVaultManager.insertNoteIntoDb() for Note type▶Note.resources → comma-separated resource_refsVaultManager.dbNoteToModel() for Note type▶resource_refs back to List<String>VaultManager.indexLinks() — extract transclusions▶TransclusionExtractor + link_type = "transclusion"VaultManager.indexLinks() — index Note.resources▶createResourceWithNote() to VaultManager▶VaultIndexer — transclusion link indexingTransclusionResolver.kt▶ with rendered content at display timeNoteType.NOTE to CLI CreateCommand + --resources optionCreateCommand — steel create resource auto-creates companion Note▶createResourceWithNote()ListCommand icon for Note typeReadCommand — resolve transclusions for display▶ for --json outputnote, remove synthesissteel migrate syntheses command▶syntheses/*.md → notes/*.md, remaps IDs and fieldssteel migrate backfill-resources — creates Notes for orphan ResourcesNoteDetailView.swift — render transclusions inline▶VaultView.swift — add Note type to filter, icons, colors▶StatusBadge.swift — add NoteStatus color/label mapping▶EditorView.swift — create/edit Notes▶NoteType.directory for file pathsCaptureView.swift — route captures through Resource + Note pair▶createResourceWithNote() to create Resource + companion NoteSharedCoreService.swift — add resolveTransclusions() method▶resolveTransclusions() via TransclusionResolver, extractTransclusionIds(), createResourceWithNote()Theme.swift — add Note type icon, color, display name▶doc.text.fill icon, blue noteColor, NoteStatus colors (active/parked)resources/ + notes/steel add to create inbox capture that produces Resource + Note pairGoal: Replace the plain TextEditor with a rich editing experience. Users can type markdown syntax (which renders live) or apply formatting via a floating toolbar. iOS 26's native TextEditor + AttributedString is the foundation.
Architecture decision: Build on iOS 26's native TextEditor with AttributedString and AttributedTextSelection — no WebView layer, no third-party dependencies. Formatting operations (toggle bold, insert link, etc.) live in the shared Kotlin module; platform-native code handles the actual text rendering and input.
Cross-platform strategy (Pattern 1): Shared Kotlin module owns the document model and formatting operations. Swift handles the AttributedString editor on iOS. Android rich editor is in Phase 8F. ~30-40% code sharing on logic, fully native editing feel on each platform.
MarkdownSpan sealed class (bold, italic, code, heading, link, highlight)▶MarkdownDocument.kt — PlainSpan, BoldSpan, ItalicSpan, BoldItalicSpan, CodeSpan, LinkSpan, WikilinkSpan, HighlightSpanMarkdownDocument model (list of MarkdownBlock with spans)▶MarkdownParser — markdown string → MarkdownDocument▶MarkdownDocumentParser.kt — block + inline span parsingMarkdownSerializer — MarkdownDocument → markdown string▶MarkdownDocumentSerializer.kt — uses block rawText for round-trip fidelityFormattingEngine — toggle operations on spans▶FormattingEngine.kt — toggleBold/Italic/Code/Highlight, toggleHeading, toggleListItem, toggleBlockquoteinsertTransclusion(id)▶ on own line with surrounding newlinesinsertNoteLink(id, title)▶[[id]] at cursorRichEditorView.swift — TextEditor with formatting toolbar▶FormattingToolbar shown when focusedFormattingToolbar.swift — floating toolbar above keyboard▶FormattingEngine▶FormattingEngine methodsAttributedTextSelection — current editor uses plain text + toolbar[[ autocomplete — search vault items, insert note link▶[[id]] navigation linkslink via FormattingEngine.insertLink()AttributedString → markdown serializer for save▶AttributeScope for highlight color▶NavigationStack push▶.navigationDestination instead of .sheetEditorView.swift — replace plain TextEditor▶RichEditorView replaces Form body section, metadata in compact headerUnifiedEditorViewController.swift wraps the existing inputAccessoryView toolbar in a transparent container to preserve styling while hovering 8pt above the keyboardUnifiedEditorView.swift now sends UITextInput delegate notifications in a consistent selection/text order for manual editorStorage edits, preventing stuck-uppercase behaviorMarkdownEditorStorage.splitLines(_:) now preserves trailing empty lines so newline edits keep display text and source offsets aligned (fixes EditorStorageIntegrationTests.interleavedEdits)EditorStorageIntegrationTests now retains NSTextStorage strongly per test storage instance and clamps plain-edit ranges, fixing the immutable-string crash and fast-path index-0 regressionsOffsetMap.sourceNSRangeForEdit(for:) now uses insertion-aware boundary mapping, and both MarkdownEditorStorage.applyDisplayEdit and UnifiedEditorView fast-path reconciliation route through it so autocorrect replacements stay inside the intended markdown span* vs **)▶FormattingEngine now matches only exact wrapper runs (not substrings inside longer marker runs), so italic toggles on bold text produce proper overlap (e.g. **word** → ***word***)MarkdownEditorStorage.rebuildAll() now supports recursive teardown of tagged attachment root views, but only when shouldTeardownAttachmentViewsOnNextRebuild is set by drag/drop mutation paths (to avoid EXC_BAD_ACCESS during normal layout-driven rebuilds). It also coalesces re-entrant rebuild requests (isRebuilding + pendingRebuild) so note-open flows that trigger setBodyText and viewDidLayoutSubviews rebuilds back-to-back do not race TextKit. ImageAttachmentProvider.loadView() explicitly detaches any prior root view before rebuilding to prevent stale provider views persisting on screen> prefix toggleattachments/editor-.../photo-1.jpg and inserts markdown image references at the cursorUndoManager integrationUITextPosition state.keyboardShortcut modifiers on toolbar actions-exportArchive … -allowProvisioningUpdates. The cert is **Cloud Managed Apple Distribution**, so it will not appear in security find-identity — the private key lives on Apple's servers and is fetched at export. Expires 2027-08-17com.steelnotes.app. TestFlight uploads fail without a record, which is separate from registering the bundle ID. Note the store name must be globally unique — "Steel" is likely taken; this is independent of CFBundleDisplayNameCloud signing permission error and a misleading No profiles ... were found underneath. The .p8 downloads exactly onceAPPSTORE_API_PRIVATE_KEY, APPSTORE_API_KEY_ID, APPSTORE_ISSUER_ID secrets▶GOOGLE_BOOKS_API_KEY secret▶main▶.github/workflows/deploy-ios.yml runs automatically on merge once the secrets exist. The earlier run 32088036309 archived and tested green, then failed at export on the API key's role — a local export succeeding proves nothing there: it authenticates as the Xcode-signed-in Apple ID, CI as the keyITSAppUsesNonExemptEncryption: false is already set in iosApp/project.ymlDELETE /account is implemented and covered by server/tests/account_deletion.rs