···11+# Auth & Account Management
22+33+## OAuth 2.1 Loopback Flow
44+55+Uses `jacquard::oauth` with `LoopbackConfig` to authenticate:
66+77+1. User enters handle or DID
88+2. Resolve authorization server via `jacquard_oauth::resolver`
99+3. Build `AtprotoClientMetadata` with app identity
1010+4. `OAuthClient` initiates PAR + DPoP flow
1111+5. Loopback server captures redirect on `127.0.0.1:<port>`
1212+6. Exchange code for tokens; `OAuthSession` manages refresh automatically
1313+1414+No app passwords needed — full OAuth 2.1 with DPoP proof-of-possession but app passwords
1515+should be supported in dev environments.
1616+1717+## Multi-Account
1818+1919+- SQLite table `accounts`: `did TEXT PK, handle TEXT, pds_url TEXT, active INTEGER`
2020+- Encrypted token storage via `jacquard_oauth::authstore` trait with a persistent implementation backed by SQLite + OS keychain (Tauri's `tauri-plugin-keychain` or raw `security-framework`)
2121+- Account switcher in sidebar — click to swap active session
2222+- Each account gets its own `OAuthSession` instance
2323+- Active account DID stored in app state; Tauri events notify frontend on switch
2424+2525+## UX Polish
2626+2727+- Login form: `Motion` spring animation on the handle input shake for invalid input
2828+- Account switcher: `Presence` exit/enter animation when swapping active account avatar
2929+- Session expiry: inline re-auth prompt with gentle pulse animation, not a modal wall
3030+- Loading: skeleton shimmer on profile card while session restores
3131+3232+## Session Lifecycle
3333+3434+- On launch: load stored sessions, attempt token refresh for active account
3535+- On token expiry: `jacquard::oauth` auto-refreshes via DPoP-bound refresh token
3636+- On refresh failure: prompt re-auth, mark account as expired in UI
3737+- Logout: revoke tokens, clear stored auth data
3838+3939+## at:// Deep Link Registration
4040+4141+- Register `at` scheme via `tauri-plugin-deep-link`
4242+- On `at://` link open: parse URI, route to AT Explorer view
4343+- If app not running: launch, then navigate after session restore
+64
docs/specs/explorer.md
···11+# AT Explorer (pds.ls-style)
22+33+A built-in browser for AT Protocol data, inspired by [pds.ls](https://pds.ls/).
44+This is the view that opens when handling `at://` URIs.
55+66+## Navigation Model
77+88+URL-bar style input accepting:
99+1010+- `at://` URIs → route directly to record/collection/repo
1111+- Handles (`@user.bsky.social`) → resolve DID → show repo
1212+- DIDs (`did:plc:...`) → show repo
1313+- PDS URLs (`https://pds.example.com`) → list hosted repos
1414+1515+## Views
1616+1717+### PDS View
1818+1919+- List accounts hosted on a PDS
2020+- Show PDS metadata (version, invite codes status)
2121+- Endpoint: `com.atproto.server.describeServer`
2222+2323+### Repository View
2424+2525+- List all collections in a repo (e.g., `app.bsky.feed.post`, `app.bsky.feed.like`)
2626+- Show repo metadata: DID, handle, PDS URL
2727+- Endpoint: `com.atproto.repo.describeRepo`, `com.atproto.sync.getRepo`
2828+2929+### Collection View
3030+3131+- Paginated list of records in a collection
3232+- Endpoint: `com.atproto.repo.listRecords`
3333+3434+### Record View
3535+3636+- Full JSON display of a single record
3737+- Render known types nicely (posts → rich text, likes → linked post, follows → profile card)
3838+- Show CID, rkey, timestamps
3939+- Endpoint: `com.atproto.repo.getRecord`
4040+4141+## Additional Features
4242+4343+- **Backlinks**: show records that reference the current record (requires relay/constellation)
4444+- **Jetstream live view**: stream new records in real-time via `jacquard::jetstream`
4545+- **CAR export**: download repo as CAR archive via `com.atproto.sync.getRepo`
4646+- **Moderation labels**: query and display labels via `com.atproto.label.queryLabels`
4747+- **Breadcrumb navigation**: `PDS > Repo > Collection > Record` with back/forward
4848+4949+## Keyboard Shortcuts
5050+5151+| Key | Action |
5252+| ----------------- | ----------------------------------- |
5353+| `Cmd+L` | Focus explorer URL bar |
5454+| `Backspace` | Navigate up one level in breadcrumb |
5555+| `Cmd+[` / `Cmd+]` | Back / forward |
5656+5757+## UX Polish
5858+5959+- View transitions: `Presence` crossfade when navigating between PDS → repo → collection → record
6060+- Jetstream live-tail: new records `Motion` slide-in from top with fade
6161+- JSON record view: syntax-highlighted with collapsible sections
6262+- Breadcrumb segments animate width via `Motion` on navigation
6363+- Skeleton screens for each view level while loading
6464+- Error inline with retry, not a blocking modal
+58
docs/specs/mvp.md
···11+# Lazurite Desktop — MVP Spec
22+33+A native desktop BlueSky/AT Protocol client built with **Tauri v2** (Rust) + **SolidJS**, focused on power-user features: multi-account, local semantic search, AT Protocol data exploration, and long-form content via standard.site lexicons.
44+55+## Architecture
66+77+```text
88+┌─────────────────────────────────────────────┐
99+│ SolidJS Frontend (WebView) │
1010+│ ├─ Timeline / Feed views │
1111+│ ├─ AT Explorer (pds.ls-style) │
1212+│ ├─ Search UI (FTS + semantic) │
1313+│ └─ Account switcher │
1414+├─────────────────────────────────────────────┤
1515+│ Tauri IPC (Commands + Events) │
1616+├─────────────────────────────────────────────┤
1717+│ Rust Backend │
1818+│ ├─ jacquard — XRPC client + types │
1919+│ ├─ jacquard::oauth — OAuth 2.1 loopback │
2020+│ ├─ rusqlite + sqlite-vec — local storage │
2121+│ ├─ fastembed — nomic-embed-text │
2222+│ └─ tauri plugins — deep-link, sql, etc │
2323+└─────────────────────────────────────────────┘
2424+```
2525+2626+## Key Dependencies
2727+2828+| Crate / Lib | Purpose |
2929+| ------------------------ | ---------------------------------------------------------------------- |
3030+| `jacquard` | AT Protocol XRPC client, zero-copy types, session management |
3131+| `jacquard::oauth` | OAuth 2.1 with DPoP, PKCE, PAR; loopback flow via `LoopbackConfig` |
3232+| `rusqlite` (bundled) | Local SQLite database |
3333+| `sqlite-vec` | Vector similarity search extension for SQLite |
3434+| `fastembed` | Local ONNX inference for `nomic-embed-text-v1.5` embeddings |
3535+| `tauri-plugin-deep-link` | Register `at://` URI scheme handler |
3636+| `tauri-plugin-sql` | Optional — frontend-side DB queries |
3737+| `solid-motionone` | Animation primitives (`Motion`, `Presence`) for SolidJS via Motion One |
3838+3939+## Cross-Cutting Concerns
4040+4141+- **Theme**: dark/light mode synced with OS, applied globally
4242+- **Keyboard shortcuts**: Aeronaut-inspired, registered per-view (see individual specs)
4343+- **Error UX**: toast notifications for transient errors, inline retry for network failures
4444+- **Loading states**: skeleton screens for feeds/lists, spinners for actions
4545+- **Accessibility**: ARIA labels, keyboard focus management, screen reader support
4646+- **Animations** (`solid-motionone`): used throughout for transitions and micro-interactions (see individual specs for specifics)
4747+- **Auto-update**: `tauri-plugin-updater` checking GitHub Releases
4848+- **Packaging**: macOS code signing, notarization, DMG distribution
4949+5050+## Feature Modules
5151+5252+Details in sub-specs:
5353+5454+- [Authentication & Accounts](./auth.md)
5555+- [Timeline & Social](./timeline.md)
5656+- [AT Explorer](./explorer.md)
5757+- [Search & Embeddings](./search.md)
5858+- [standard.site Integration](./standard-site.md)
+76
docs/specs/search.md
···11+# Search & Embeddings
22+33+Local full-text + semantic search over the authenticated user's saved and liked posts.
44+55+## Data Pipeline
66+77+1. **Sync**: on login and periodically, fetch user's likes (`app.bsky.feed.getActorLikes`) and bookmarks. Paginate fully, store in SQLite.
88+2. **Index FTS**: insert post text into SQLite FTS5 virtual table for keyword search.
99+3. **Embed**: run post text through `fastembed` with `nomic-embed-text-v1.5` (768-dim). Store vectors in `sqlite-vec` virtual table.
1010+4. **Incremental**: track cursor/last-seen; only process new posts on subsequent syncs.
1111+1212+## SQLite Schema
1313+1414+```sql
1515+-- Post storage
1616+CREATE TABLE posts (
1717+ uri TEXT PRIMARY KEY,
1818+ cid TEXT NOT NULL,
1919+ author_did TEXT NOT NULL,
2020+ author_handle TEXT,
2121+ text TEXT,
2222+ created_at TEXT,
2323+ indexed_at TEXT DEFAULT CURRENT_TIMESTAMP,
2424+ json_record TEXT, -- full record JSON
2525+ source TEXT NOT NULL -- 'like', 'bookmark', 'own'
2626+);
2727+2828+-- Full-text search
2929+CREATE VIRTUAL TABLE posts_fts USING fts5(text, uri UNINDEXED, content=posts, content_rowid=rowid);
3030+3131+-- Vector embeddings
3232+CREATE VIRTUAL TABLE posts_vec USING vec0(
3333+ uri TEXT PRIMARY KEY,
3434+ embedding float[768]
3535+);
3636+```
3737+3838+## Search Modes
3939+4040+| Mode | How |
4141+| -------- | ----------------------------------------------------------------------------------------- |
4242+| Keyword | `SELECT * FROM posts_fts WHERE posts_fts MATCH ?` |
4343+| Semantic | Embed query → `SELECT * FROM posts_vec WHERE embedding MATCH ? ORDER BY distance LIMIT k` |
4444+| Hybrid | Run both, merge results by reciprocal rank fusion |
4545+4646+## Embedding Details
4747+4848+- Model: `nomic-embed-text-v1.5` via `fastembed` (ONNX runtime, no GPU required)
4949+- Dimensions: 768 (or 256 with Matryoshka truncation for speed)
5050+- Batch embedding on sync; single embedding on search query
5151+- Model downloaded on first use, cached in Tauri app data dir
5252+5353+## Tauri Commands
5454+5555+```rs
5656+search_posts(query: String, mode: "keyword"|"semantic"|"hybrid", limit: u32) -> Vec<PostResult>
5757+sync_liked_posts(did: String) -> SyncStatus
5858+get_sync_status(did: String) -> SyncStatus
5959+```
6060+6161+## Keyboard Shortcuts
6262+6363+| Key | Action |
6464+| -------- | ----------------------------------------------- |
6565+| `/` | Focus search bar from anywhere |
6666+| `Tab` | Cycle search mode (keyword → semantic → hybrid) |
6767+| `Escape` | Clear search / close results |
6868+6969+## UX Polish
7070+7171+- Search results: staggered `Motion` fade-in list animation
7272+- Mode switcher: `Motion` sliding indicator underline between tabs
7373+- Sync status: animated progress bar during sync, `Presence` fade-out when complete
7474+- Highlighted keyword matches in result text
7575+- Model download: progress bar on first launch with percentage + ETA
7676+- Empty state: illustration with prompt when no posts synced yet
+43
docs/specs/standard-site.md
···11+# Standard.site Integration
22+33+Display long-form content for any handle using [standard.site](https://standard.site) lexicons.
44+55+## Lexicons
66+77+| Lexicon | Purpose |
88+| ---------------------------------- | -------------------------------------------------------------- |
99+| `site.standard.publication` | Publication metadata: name, description, icon, base URL, theme |
1010+| `site.standard.document` | Individual document/post: title, content, metadata |
1111+| `site.standard.graph.subscription` | User subscriptions to publications |
1212+1313+## Feature: View Publications for a Handle
1414+1515+1. Given a handle, resolve DID
1616+2. Query `com.atproto.repo.listRecords` for collection `site.standard.publication`
1717+3. If found, display publication card (name, description, icon)
1818+4. List documents via `site.standard.document` collection
1919+ - Leaflet
2020+ - PCKT
2121+ - Offprint
2222+ - Greengale
2323+ - Bento
2424+5. Render document content (markdown) in a reading view
2525+2626+## Feature: Subscribe to Publications
2727+2828+- Authenticated users can create `site.standard.graph.subscription` records
2929+- Track subscriptions in sidebar alongside feed list
3030+3131+## Integration Points
3232+3333+- AT Explorer: when browsing a repo, highlight standard.site collections with a distinct icon
3434+- Profile view: show "Publications" tab if the user has standard.site records
3535+- Search: index document text alongside posts for FTS/semantic search
3636+3737+## UX Polish
3838+3939+- Publication cards: `Motion` scale-up on hover, spring easing
4040+- Document list: staggered `Motion` fade-in
4141+- Reading view: `Presence` slide-in from right (like turning a page)
4242+- Subscribe/unsubscribe: `Motion` pop on the icon toggle
4343+- Markdown content: smooth typography with comfortable reading width
+66
docs/specs/timeline.md
···11+# Timeline & Social Features
22+33+## Timeline View
44+55+Inspired by Aeronaut's timeline continuity — new posts prepend without losing scroll position.
66+77+### XRPC Endpoints (via jacquard)
88+99+| Action | Lexicon |
1010+| ------------------ | --------------------------------------------------------- |
1111+| Following timeline | `app.bsky.feed.getTimeline` |
1212+| Custom feed | `app.bsky.feed.getFeed` |
1313+| Author feed | `app.bsky.feed.getAuthorFeed` |
1414+| Post thread | `app.bsky.feed.getPostThread` |
1515+| Like a post | `app.bsky.feed.like` (create record) |
1616+| Repost | `app.bsky.feed.repost` (create record) |
1717+| Create post | `com.atproto.repo.createRecord` with `app.bsky.feed.post` |
1818+| Get likes list | `app.bsky.feed.getActorLikes` |
1919+| Get profile | `app.bsky.actor.getProfile` |
2020+| Follow/unfollow | `app.bsky.graph.follow` (create/delete record) |
2121+| Mute/block | `app.bsky.graph.muteActor` / `app.bsky.graph.block` |
2222+2323+### Feed Preferences
2424+2525+- Toggle reposts, replies, quote-posts per feed (like Aeronaut)
2626+- Store preferences per account in SQLite
2727+2828+## Post Composer
2929+3030+- Rich text via `jacquard::richtext` — auto-detect mentions, links, hashtags
3131+- Image/media upload via `com.atproto.repo.uploadBlob`
3232+- Reply threading with parent/root refs
3333+- Quote post embed
3434+3535+## Notifications
3636+3737+- `app.bsky.notification.listNotifications` — poll or use Jetstream
3838+- Separate tabs: Mentions vs Activity (Aeronaut pattern)
3939+- System notifications via Tauri
4040+4141+## Keyboard Shortcuts
4242+4343+| Key | Action |
4444+| ------------- | ------------------------ |
4545+| `n` | New post (open composer) |
4646+| `j` / `k` | Next / previous post |
4747+| `l` | Like focused post |
4848+| `r` | Reply to focused post |
4949+| `t` | Repost focused post |
5050+| `o` / `Enter` | Open thread |
5151+| `1`–`9` | Switch between feeds |
5252+5353+## UX Polish
5454+5555+- New posts slide in from top via `Motion` with spring easing; scroll position preserved
5656+- Like/repost actions: `Motion` scale pop on the icon (1.0 → 1.3 → 1.0)
5757+- Post card: subtle `Motion` fade-in on viewport enter during infinite scroll
5858+- Composer: `Presence` slide-up animation on open, slide-down on dismiss
5959+- Feed switcher: `Presence` crossfade between feed content on tab change
6060+- Skeleton screens while feeds load; error toast with retry button on network failure
6161+- Feed preferences stored per account in SQLite
6262+6363+## Direct Messages
6464+6565+- `chat.bsky.convo.*` lexicons for DM support
6666+- Deferred to post-MVP unless trivial to add
+17
docs/tasks/01-backend-setup.md
···11+# Task 01: Rust Backend Setup
22+33+Spec: [mvp.md](../specs/mvp.md)
44+55+## Steps
66+77+- [ ] Add Cargo dependencies: `jacquard`, `rusqlite` (bundled), `sqlite-vec`, `fastembed`, `tokio`
88+- [ ] Add Tauri plugins: `tauri-plugin-deep-link`, `tauri-plugin-notification`, `tauri-plugin-updater`
99+- [ ] Add frontend deps: `solid-motionone` (animation), install via npm
1010+- [ ] Create `src-tauri/src/db.rs` — initialize SQLite, run migrations, load `sqlite-vec` extension
1111+- [ ] Create migration system: `accounts`, `posts`, `posts_fts`, `posts_vec` tables
1212+- [ ] Create `src-tauri/src/state.rs` — `AppState` struct holding DB pool, active session, account list
1313+- [ ] Register `AppState` as Tauri managed state
1414+- [ ] Create Tauri command scaffold with error handling pattern (`Result<T, String>` or custom error type)
1515+- [ ] Set up dark/light theme: CSS custom properties, OS preference detection via `prefers-color-scheme`
1616+- [ ] Create global error toast component using `Presence` for enter/exit animations
1717+- [ ] Verify build compiles on macOS with `cargo tauri dev`
+23
docs/tasks/02-auth.md
···11+# Task 02: Auth & Accounts
22+33+Spec: [auth.md](../specs/auth.md)
44+55+## Steps
66+77+- [ ] Implement `PersistentAuthStore` backed by SQLite (impl `jacquard_oauth::authstore` trait)
88+- [ ] Create Tauri command `login(handle: String)`:
99+ - Resolve handle → authorization server
1010+ - Build `AtprotoClientMetadata` for Lazurite
1111+ - Start loopback OAuth via `LoopbackConfig`
1212+ - Store session tokens, insert into `accounts` table
1313+ - Return account info to frontend
1414+- [ ] Create Tauri command `logout(did: String)` — revoke tokens, remove from DB
1515+- [ ] Create Tauri command `switch_account(did: String)` — swap active `OAuthSession` in state
1616+- [ ] Create Tauri command `list_accounts()` → `Vec<Account>`
1717+- [ ] On app launch: restore sessions from DB, auto-refresh tokens for active account
1818+- [ ] Register `at://` scheme via deep-link plugin in `tauri.conf.json`
1919+- [ ] Handle deep-link events: parse `at://` URI, emit Tauri event to frontend for navigation
2020+- [ ] **Frontend**: login form with `Motion` spring shake on invalid handle
2121+- [ ] **Frontend**: account switcher dropdown in sidebar with `Presence` avatar enter/exit
2222+- [ ] **Frontend**: skeleton shimmer on profile card during session restore
2323+- [ ] **Frontend**: inline re-auth prompt with pulse animation on session expiry