···11+# Drafts
22+33+## Overview
44+55+Draft posts are composed locally and persisted to SQLite, allowing users to save work-in-progress posts and resume them later. Drafts survive app restarts and account switches.
66+77+AT Protocol has no concept of private or unpublished records today — all repo data is public and broadcast on the firehose. A "Permissioned Data" initiative is the protocol team's top priority for summer 2026, which may eventually enable server-side drafts. Until then, drafts are local-only. The schema mirrors `app.bsky.feed.post` fields to make future migration straightforward.
88+99+Every other BlueSky client with drafts (Skeets, deck.blue) also uses client-local storage.
1010+1111+## Data Model
1212+1313+Drafts live in a `drafts` SQLite table, scoped per account:
1414+1515+| Column | Type | Notes |
1616+| ------------------ | ------------------ | ----------------------------------------------------- |
1717+| `id` | `TEXT PRIMARY KEY` | UUID |
1818+| `account_did` | `TEXT NOT NULL` | Owning account |
1919+| `text` | `TEXT NOT NULL` | Post body (may be empty string for embed-only drafts) |
2020+| `reply_parent_uri` | `TEXT` | Parent post URI if replying |
2121+| `reply_parent_cid` | `TEXT` | Parent post CID |
2222+| `reply_root_uri` | `TEXT` | Root post URI if replying |
2323+| `reply_root_cid` | `TEXT` | Root post CID |
2424+| `quote_uri` | `TEXT` | Quoted post URI if quote-posting |
2525+| `quote_cid` | `TEXT` | Quoted post CID |
2626+| `title` | `TEXT` | Optional user label for organizing drafts |
2727+| `created_at` | `TEXT NOT NULL` | ISO 8601 creation timestamp |
2828+| `updated_at` | `TEXT NOT NULL` | ISO 8601 last-modified timestamp |
2929+3030+Media/blob references are excluded from v1 — they require `uploadBlob` which returns ephemeral blob refs that expire. Drafts with intended media should note this in the UI.
3131+3232+## Commands
3333+3434+| Command | Args | Returns | Notes |
3535+| -------------- | ------------- | -------------------- | ---------------------------------------------------- |
3636+| `list_drafts` | `account_did` | `Vec<Draft>` | Ordered by `updated_at` desc |
3737+| `get_draft` | `id` | `Draft` | Single draft by ID |
3838+| `save_draft` | `DraftInput` | `Draft` | Upsert — creates or updates based on `id` presence |
3939+| `delete_draft` | `id` | `()` | Hard delete |
4040+| `submit_draft` | `id` | `CreateRecordResult` | Load draft → `create_post` → delete draft on success |
4141+4242+`DraftInput` contains all writable fields (text, reply refs, quote ref, title). If `id` is provided, it updates; otherwise it creates with a new UUID.
4343+4444+## Autosave
4545+4646+The composer autosaves to a draft after 3 seconds of inactivity (debounced). An active autosave draft is marked by storing its `id` in the composer state. When the user submits or explicitly discards, the autosave draft is deleted.
4747+4848+On app launch, if an autosave draft exists for the active account, the composer offers to restore it via a non-blocking toast: _"You have an unsaved post. Restore?"_ with Restore / Discard actions.
4949+5050+## UI
5151+5252+### Drafts List
5353+5454+Accessible from a button in the composer header or toolbar. Opens a `Presence` slide-up panel listing all drafts for the active account.
5555+5656+Each draft card shows:
5757+5858+- Title (or text preview if no title)
5959+- Reply/quote context indicator (icon + truncated parent)
6060+- Relative timestamp ("2 hours ago")
6161+- Delete action (with confirmation)
6262+6363+Tap a draft to load it into the composer, replacing current content (with confirmation if composer is non-empty).
6464+6565+### Composer Integration
6666+6767+- Autosave indicator in composer footer: subtle "Saved" / "Saving..." text
6868+- "Save as draft" button in composer header (explicit save + close)
6969+- Draft count badge on the drafts list button when drafts exist
7070+- When loading a draft into the composer, the draft ID is tracked so subsequent saves update the same draft rather than creating duplicates
7171+7272+### Keyboard Shortcuts
7373+7474+| Key | Action |
7575+| ------------ | ----------------------------------------------- |
7676+| `Ctrl/Cmd+S` | Explicit save current composer content as draft |
7777+| `Ctrl/Cmd+D` | Open drafts list |
7878+7979+## Constraints
8080+8181+- **No cross-device sync**: Drafts are local SQLite. If Permissioned Data ships, drafts could migrate to private repo records.
+48
docs/tasks/14-drafts.md
···11+# Milestone 14: Draft Posts
22+33+Spec: [drafts.md](../specs/drafts.md)
44+55+Depends on: Milestone 03 (Feeds — composer, `create_post`)
66+77+## Steps
88+99+### Backend - `src-tauri/src/drafts.rs` + `src-tauri/src/commands/drafts.rs`
1010+1111+- [ ] SQLite migration: `drafts` table (`id TEXT PRIMARY KEY, account_did TEXT NOT NULL, text TEXT NOT NULL, reply_parent_uri TEXT, reply_parent_cid TEXT, reply_root_uri TEXT, reply_root_cid TEXT, quote_uri TEXT, quote_cid TEXT, title TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL`)
1212+- [ ] `Draft` and `DraftInput` structs mirroring the schema
1313+- [ ] `list_drafts(account_did: String)` — return all drafts for the account, ordered by `updated_at` desc
1414+- [ ] `get_draft(id: String)` — single draft by ID
1515+- [ ] `save_draft(input: DraftInput)` — upsert: if `id` is present and exists, update; otherwise insert with new UUID
1616+- [ ] `delete_draft(id: String)` — hard delete
1717+- [ ] `submit_draft(id: String)` — load draft, call `create_post`, delete draft on success, return `CreateRecordResult`
1818+1919+### Frontend - Drafts List Panel
2020+2121+- [ ] Drafts list panel component with `Presence` slide-up from composer
2222+- [ ] Draft cards: title or text preview, reply/quote context indicator, relative timestamp, delete button
2323+- [ ] Tap draft to load into composer (confirmation if composer has content)
2424+- [ ] Delete with confirmation
2525+- [ ] Empty state: *"No drafts yet. Saved posts will appear here."*
2626+- [ ] `Ctrl/Cmd+D` keyboard shortcut to open drafts list
2727+2828+### Frontend - Composer Integration
2929+3030+- [ ] Autosave: debounced (3s inactivity) save to draft while composing, tracked by draft `id` in composer state
3131+- [ ] Autosave indicator in composer footer: "Saved" / "Saving..." text
3232+- [ ] "Save as draft" button in composer header — explicit save + close composer
3333+- [ ] Draft count badge on drafts list button
3434+- [ ] `Ctrl/Cmd+S` keyboard shortcut to save current composer as draft
3535+- [ ] On app launch, detect unsaved autosave draft → toast: *"You have an unsaved post. Restore?"* with Restore / Discard
3636+3737+### Frontend - Draft Lifecycle
3838+3939+- [ ] Loading a draft into the composer tracks the draft `id` so subsequent autosaves update (not duplicate)
4040+- [ ] Successful post submission deletes the associated draft
4141+- [ ] Explicit discard from composer deletes the autosave draft
4242+- [ ] Account switch clears composer state; autosave draft persists for the original account
4343+4444+### Parking Lot
4545+4646+- [ ] Media attachments in drafts (requires local blob caching + re-upload on submit)
4747+- [ ] Thread builder (compose multi-post threads as a single draft)
4848+- Cross-device sync via AT Protocol Permissioned Data (blocked on protocol — expected summer 2026)