···11+---
22+title: Notification Architecture
33+updated: 2026-04-29
44+---
55+66+## Summary
77+88+Lazurite currently supports in-app notifications (alerts feed + unread badge), but
99+it does not deliver OS-level notifications when the app is backgrounded or killed.
1010+This spec defines a defensive, staged path from polling-only behavior to robust
1111+push + local notification delivery.
1212+1313+## Current State (Lazurite)
1414+1515+What exists today:
1616+1717+- `app.bsky.notification.listNotifications` for alerts feed
1818+- `app.bsky.notification.getUnreadCount` polled every 30s in foreground
1919+- `app.bsky.notification.updateSeen` on read/open flows
2020+- `workmanager` already integrated for scheduled posts
2121+2222+What is missing:
2323+2424+- No device push token lifecycle
2525+- No `registerPush` / `unregisterPush` integration
2626+- No local notification renderer/channels/categories
2727+- No durable dedupe state for "already notified" events
2828+- No background notification sync worker
2929+3030+## Research Findings
3131+3232+### Bluesky notification APIs
3333+3434+Core endpoints (auth required):
3535+3636+- `app.bsky.notification.listNotifications`
3737+ - Params include `cursor`, `limit` (1-100), optional `reasons`, optional `seenAt`
3838+ - Response includes `notifications[]`, optional `cursor`, optional `seenAt`
3939+- `app.bsky.notification.getUnreadCount`
4040+ - Returns unread `count`
4141+- `app.bsky.notification.updateSeen`
4242+ - Marks notifications as seen at a timestamp
4343+- `app.bsky.notification.registerPush`
4444+ - Required body: `serviceDid`, `token`, `platform`, `appId`
4545+ - Optional body: `ageRestricted`
4646+- `app.bsky.notification.unregisterPush`
4747+ - Required body: `serviceDid`, `token`, `platform`, `appId`
4848+- `app.bsky.notification.putPreferencesV2`
4949+ - Server-side notification preference controls (follow/like/reply/etc.)
5050+5151+Important nuance:
5252+5353+- Official Bluesky app passes an `atproto-proxy` header for push registration:
5454+ `did:web:api.bsky.app#bsky_notif`
5555+- Official Bluesky app uses `serviceDid: did:web:api.bsky.app` (or staging DID)
5656+5757+### Reference implementation patterns
5858+5959+Strong patterns worth reusing:
6060+6161+- Android FCM entrypoint (`FirebaseMessagingService`) parses push payload keys:
6262+ `senderDid`, `targetDid`, `recordUri`, `reason`
6363+- Push payload is treated as a trigger, not trusted display content:
6464+ - Resolve full record via authenticated API
6565+ - Apply moderation/filtering before display
6666+- Defensive processing contract:
6767+ - Timeout-bound notification processing (10s)
6868+ - Explicit processed/dropped ack state
6969+ - Dedup by stable notification identifiers
7070+- Permission UX:
7171+ - Runtime request + rationale + settings fallback
7272+- Delivery UX:
7373+ - Channel per reason family (likes/replies/follows/etc.)
7474+ - Deep links for post/profile targets
7575+7676+Architecture-specific behavior to avoid coupling to:
7777+7878+- Routing token registration through a custom backend endpoint can be valid, but
7979+ it is optional and not required for a direct Bluesky `registerPush` strategy.
8080+8181+### Platform/Flutter constraints
8282+8383+- Android 13+: `POST_NOTIFICATIONS` runtime permission required
8484+- Android periodic background work minimum interval is 15 minutes
8585+- Android exact-alarm behavior tightened on Android 14 (not suitable as default)
8686+- iOS background execution is system-managed and non-deterministic
8787+ (`earliestBeginDate` is not a guarantee)
8888+- iOS background push updates are low priority and can be throttled
8989+- Flutter background handlers must be top-level entry points
9090+ (`@pragma('vm:entry-point')` where required)
9191+9292+## Assumptions and Open Questions
9393+9494+Assumptions (to validate during implementation):
9595+9696+- Lazurite can register directly against Bluesky notification service DID
9797+ (`did:web:api.bsky.app`) using `atproto-proxy: ...#bsky_notif`.
9898+- Notification payload and reason mapping from Bluesky are stable enough to map
9999+ into local channel/category policy.
100100+101101+Open questions:
102102+103103+- Multi-account policy: should each account register separate token records?
104104+- Opt-out semantics: unregister on logout vs keep per-account registration?
105105+- Should we support Android foreground service fallback for tighter latency, or
106106+ accept periodic/background best-effort only?
107107+- Do we expose per-reason push toggles locally first, or defer to server-side
108108+ `putPreferencesV2` only?
109109+110110+## Architecture Options
111111+112112+### Option A: Polling only (foreground + background)
113113+114114+Pros:
115115+116116+- No FCM/APNs setup required
117117+- Simpler backend story
118118+119119+Cons:
120120+121121+- Delayed delivery (>=15 min in background)
122122+- iOS execution unpredictability
123123+- Higher API/battery overhead
124124+125125+### Option B: Push only
126126+127127+Pros:
128128+129129+- Fastest delivery
130130+- Lower polling load
131131+132132+Cons:
133133+134134+- Requires strict token lifecycle and permission handling
135135+- Delivery depends on push transport + payload correctness
136136+137137+### Option C: Hybrid (recommended)
138138+139139+- Push is primary trigger for near-real-time delivery
140140+- Polling remains as fallback and for reconciliation
141141+- Foreground unread badge polling can remain lightweight
142142+143143+## Recommended Design
144144+145145+### 1. NotificationDomainService
146146+147147+Create a domain orchestrator that all entry points call:
148148+149149+- `onForegroundTick()` (badge + optional reconcile)
150150+- `onBackgroundTick()` (reconcile window)
151151+- `onPushPayload(Map<String, String>)`
152152+- `markSeen(DateTime at)`
153153+154154+Responsibilities:
155155+156156+- Parse/validate payload defensively
157157+- Fetch canonical notification details from Bluesky
158158+- Filter by moderation + user prefs
159159+- Dedupe and persist delivery state
160160+- Trigger OS local notification display
161161+162162+### 2. Push token lifecycle
163163+164164+Add `PushRegistrationService`:
165165+166166+- Acquire platform token (FCM/APNs bridge)
167167+- Register with Bluesky `registerPush`
168168+- Re-register on token refresh, login, account switch, app upgrade
169169+- Unregister on logout/account removal (`unregisterPush`)
170170+171171+Inputs:
172172+173173+- `serviceDid` (prod/staging aware)
174174+- `platform` (`ios`/`android`)
175175+- `appId` (bundle/package identifier)
176176+- `token`
177177+178178+### 3. Local notifications
179179+180180+Use a local notification adapter abstraction with platform implementations.
181181+182182+- Android:
183183+ - Channel groups by reason family (`mentions`, `replies`, `follows`, `likes`, `misc`)
184184+ - Tap routes to `/post?uri=...` or `/profile/view?actor=...`
185185+- iOS:
186186+ - Category identifiers for future actions
187187+ - Deep link userInfo payload for route restoration
188188+189189+### 4. Dedupe + delivery state (Drift)
190190+191191+Add a new table (with migration): `notification_deliveries`
192192+193193+Suggested fields:
194194+195195+- `id` (PK)
196196+- `accountDid` (text)
197197+- `notificationUri` (text, indexed)
198198+- `notificationCid` (text nullable)
199199+- `reason` (text)
200200+- `indexedAt` (datetime)
201201+- `source` (`push|poll`)
202202+- `deliveredAt` (datetime)
203203+- `openedAt` (datetime nullable)
204204+- `dismissedAt` (datetime nullable)
205205+- unique constraint on (`accountDid`, `notificationUri`)
206206+207207+Use this table to avoid duplicate OS notifications across push and poll paths.
208208+209209+### 5. Background execution
210210+211211+Reuse existing `workmanager` foundation.
212212+213213+- Android:
214214+ - Periodic reconcile task at 15m+ cadence
215215+ - Network-connected constraint
216216+- iOS:
217217+ - Background fetch / BGTaskScheduler best-effort reconcile
218218+ - Keep tasks short and idempotent
219219+220220+### 6. Permission UX
221221+222222+- Ask only after contextual primer (alerts/home), not on first launch
223223+- On deny, show "Open Settings" path
224224+- Keep in-app alerts functional even when OS permission is denied
225225+226226+## Rollout Plan
227227+228228+### Phase N0 - Foundation hardening
229229+230230+- Extract notification orchestration service
231231+- Add delivery-state persistence + migrations
232232+- Keep behavior polling-only
233233+234234+### Phase N1 - Local notifications from polling
235235+236236+- Emit OS local notifications for new unseen items discovered via reconcile
237237+- Validate routing, dedupe, and permissions
238238+239239+### Phase N2 - Push registration and handling
240240+241241+- Add token registration/unregistration
242242+- Add background push payload handler -> fetch canonical record -> display
243243+244244+### Phase N3 - Preference integration
245245+246246+- Add server preference sync (`getPreferences` / `putPreferencesV2`)
247247+- Add local controls mapped to server fields
248248+249249+### Phase N4 - Reliability/observability
250250+251251+- Add structured notification logs + debug screen counters
252252+- Add failure metrics (token register failures, dropped payloads, dedupe suppressions)
253253+254254+## Testing Strategy
255255+256256+Required coverage per phase:
257257+258258+- Unit tests:
259259+ - Payload parsing/validation
260260+ - Dedupe decisions
261261+ - token lifecycle state machine
262262+- Bloc/cubit tests:
263263+ - Permission gating
264264+ - unread count reconcile behavior
265265+- Integration tests:
266266+ - deep link open from notification payload
267267+ - background worker reconciliation path
268268+- Manual smoke matrix:
269269+ - Android 13+ deny/allow paths
270270+ - iOS allow/deny/settings round trip
271271+ - multi-account register/unregister correctness
272272+273273+## Risks and Mitigations
274274+275275+- Risk: Duplicate alerts from push + poll race
276276+ - Mitigation: persisted dedupe with unique key on notification URI
277277+- Risk: Background handlers killed or delayed
278278+ - Mitigation: hybrid model + reconcile worker + idempotent processing
279279+- Risk: API/proxy mismatches for `registerPush`
280280+ - Mitigation: stage against test account, log raw request/response status
281281+- Risk: Permission denial degrades trust
282282+ - Mitigation: contextual request timing, clear fallback behavior
283283+284284+## Deferred (Not in initial rollout)
285285+286286+- Rich actions (reply/like from notification shade)
287287+- Notification grouping by thread/conversation
288288+- Server-driven quiet hours / digest mode
289289+- Desktop/web parity
+244
docs/specs/routing.md
···11+---
22+title: AppView Routing (Bluesky + Blacksky + microcosm Fallbacks)
33+updated: 2026-04-29
44+---
55+66+Introduce explicit AppView routing so Lazurite can target:
77+88+1. Bluesky (`did:web:api.bsky.app#bsky_appview`)
99+2. Blacksky (`did:web:api.blacksky.community#bsky_appview`)
1010+3. microcosm fallbacks for specific degraded paths (identity and backlink-style enrichments)
1111+1212+Design goal: route intentionally, fail predictably, and avoid hidden dependence on any single provider.
1313+1414+## Product Decisions
1515+1616+1. Provider selection is chosen on the login screen.
1717+2. Provider selection can be changed later in Settings, but change requires app reset.
1818+3. Cross-provider fallback (provider A -> provider B) is user-controlled, not automatic by default.
1919+4. Slingshot identity fallback is controlled by a setting.
2020+5. Provider health/capability probes run at startup, with manual refresh in Settings.
2121+6. Blacksky must be available by default on login/onboarding (no advanced-toggle gate).
2222+2323+## Current State (Lazurite)
2424+2525+- OAuth entryway is currently hardcoded to `bsky.social` in auth flow.
2626+- App-level settings already support configurable network services in some areas:
2727+ - Typeahead provider (`bluesky`/`community`)
2828+ - Constellation base URL (default `https://constellation.microcosm.blue`)
2929+- Public profile context lookups already use direct AppView host `public.api.bsky.app` in one path.
3030+- There is no shared AppView routing abstraction and no user-selectable AppView provider.
3131+3232+## Research Findings
3333+3434+### 1. Official routing expectations
3535+3636+- Bluesky docs: authenticated `app.bsky.*` requests go through the user's PDS and are proxied to AppView.
3737+- Bluesky docs: public `app.bsky.*` endpoints can be called directly on `https://public.api.bsky.app`.
3838+- AT Protocol roadmap: clients should set `atproto-proxy` explicitly and should not depend on legacy default forwarding.
3939+4040+### 2. Live AppView DID documents (verified)
4141+4242+- `https://api.bsky.app/.well-known/did.json` includes:
4343+ - `#bsky_appview` at `https://api.bsky.app`
4444+ - `#bsky_notif` at `https://api.bsky.app`
4545+- `https://api.blacksky.community/.well-known/did.json` includes:
4646+ - `#bsky_appview` at `https://api.blacksky.community`
4747+ - `#bsky_notif` at `https://api.blacksky.community`
4848+4949+### 3. Live Blacksky API compatibility (spot checks)
5050+5151+- `https://api.blacksky.community/xrpc/app.bsky.actor.getProfile?...` returns valid profile payload.
5252+- `https://api.blacksky.community/xrpc/app.bsky.unspecced.getTrends` returns trends payload.
5353+5454+### 4. microcosm services (verified)
5555+5656+- Constellation endpoint is live for backlink-style counts:
5757+ - `https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinksCount`
5858+- Slingshot identity endpoint is live:
5959+ - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc`
6060+ - Returns DID, handle, and PDS.
6161+6262+## Design
6363+6464+### AppView provider model
6565+6666+Add settings-backed provider selection:
6767+6868+- `bluesky` (default)
6969+- `blacksky`
7070+- `custom` (optional advanced path; disabled in UI until validated)
7171+7272+Selection lifecycle:
7373+7474+- Initial selection is made on the login screen before auth flow starts.
7575+- Post-login changes are allowed in Settings but must trigger an app reset flow to avoid mixed-session routing state.
7676+7777+Provider descriptor:
7878+7979+```dart
8080+class AppViewProvider {
8181+ final String key; // bluesky, blacksky, custom
8282+ final String serviceDid; // did:web:...#bsky_appview
8383+ final Uri publicBaseUrl; // public unauthenticated app.bsky host
8484+ final Uri entrywayUrl; // login/account entryway
8585+}
8686+```
8787+8888+Built-in defaults:
8989+9090+- Bluesky:
9191+ - service DID: `did:web:api.bsky.app#bsky_appview`
9292+ - public base: `https://public.api.bsky.app`
9393+ - entryway: `https://bsky.social`
9494+- Blacksky:
9595+ - service DID: `did:web:api.blacksky.community#bsky_appview`
9696+ - public base: `https://api.blacksky.community`
9797+ - entryway: `https://blacksky.app`
9898+9999+### Router abstraction
100100+101101+Introduce `AppViewRouter` as a single source of truth:
102102+103103+- `Map<String, String> appBskyProxyHeaders()`
104104+- `Uri publicEndpoint(String xrpcPath, Map<String, String> query)`
105105+- `Uri entrywayForAuth()`
106106+- `Future<AppViewHealth> probeProvider()`
107107+108108+This keeps routing policy out of individual repositories.
109109+110110+### Request routing policy
111111+112112+1. **Authenticated `app.bsky.*`**
113113+ - Route through PDS as today.
114114+ - Explicitly set `atproto-proxy` to selected provider DID.
115115+2. **Signed-out/public `app.bsky.*`**
116116+ - Call selected provider `publicBaseUrl` directly.
117117+3. **`com.atproto.*`**
118118+ - Never AppView-routed. Resolve target PDS by DID/handle as normal.
119119+120120+### Fallback policy (defensive)
121121+122122+Fallback order must be explicit and bounded:
123123+124124+1. Try selected provider.
125125+2. If user enabled "Cross-provider fallback", then on transient failure (`429`, `5xx`, timeout, DNS):
126126+ - Try alternate built-in provider for read-only public endpoints.
127127+3. For specific non-AppView enrichments:
128128+ - Backlink/social graph counts and related index lookups: Constellation.
129129+ - Identity mini-doc resolution when handle resolution is flaky: Slingshot `resolveMiniDoc` (only when enabled in settings).
130130+4. Record failure reason and chosen fallback in logs.
131131+5. Apply a short circuit-breaker window per failed provider/endpoint to prevent retry storms.
132132+133133+Do not fallback across write operations.
134134+135135+### Capability gating
136136+137137+Track endpoint support per provider to avoid blind retries:
138138+139139+- `app.bsky.actor.getProfile` (public read): bluesky + blacksky
140140+- `app.bsky.feed.getPostThread` (public read): bluesky + blacksky
141141+- `app.bsky.unspecced.getTrends` (public read): bluesky + blacksky (verify by probe)
142142+- Custom namespaces: provider-specific only
143143+144144+### Auth and account UX implications
145145+146146+- If user selects Blacksky provider, default login entryway should become `https://blacksky.app`.
147147+- Existing accounts keep current PDS/session behavior; AppView selection changes only request routing.
148148+- Add a warning in settings: provider choice affects content ranking, moderation context, and availability.
149149+- Login/onboarding must show both Bluesky and Blacksky as first-class provider options by default.
150150+- Changing provider in settings must present a reset confirmation flow.
151151+152152+### Reset UX contract (recommended)
153153+154154+Best UX contract for provider switching:
155155+156156+1. User selects a new provider in Settings.
157157+2. App shows a blocking confirmation sheet:
158158+ - "Apply and restart now"
159159+ - "Cancel"
160160+ - Message: "You will remain signed in. No local data will be deleted."
161161+3. On confirm, app performs a soft restart:
162162+ - Persist new provider selection first.
163163+ - Cancel in-flight requests.
164164+ - Tear down and rebuild app-level DI/blocs/repositories.
165165+ - Return to bootstrap/splash and rehydrate from persisted state.
166166+167167+For this phase, do not log out users and do not wipe local database on provider change.
168168+169169+### State safety requirements
170170+171171+To avoid mixed in-memory routing state:
172172+173173+1. `AppViewRouter` is the only runtime source of provider state.
174174+2. Long-lived repositories/blocs must not cache provider values separately.
175175+3. Provider switch path must block new requests until rebuild completes.
176176+4. Use a routing epoch/version so stale pre-reset responses are ignored post-reset.
177177+178178+### Login-time persistence ordering
179179+180180+To ensure provider choice is honored from first network call:
181181+182182+1. Persist login-screen provider choice before starting OAuth/app-password calls.
183183+2. Disable login submission while provider persistence is in flight.
184184+3. Construct auth/network clients only after persisted provider is available in bootstrap.
185185+186186+### Health probes
187187+188188+- Run provider health/capability probes once at startup.
189189+- Expose a manual "Refresh Provider Health" control in Settings.
190190+- Do not run periodic background probes in this phase.
191191+192192+## Adversarial checks (assumptions to challenge)
193193+194194+1. A provider advertises `#bsky_appview` but only partially implements endpoints.
195195+2. A provider endpoint is live but semantically diverges (labels, trends, moderation filters).
196196+3. Docs may lag live infrastructure (observed for Blacksky roadmap vs live API host).
197197+4. Transient success can mask long-tail reliability problems without health telemetry.
198198+199199+Mitigation:
200200+201201+- Runtime capability probes.
202202+- Endpoint-level fallback gates.
203203+- Structured logs + per-provider failure counters.
204204+205205+## Testing Strategy
206206+207207+### Unit
208208+209209+- Provider selection and normalization.
210210+- Login-time provider selection persistence + settings-change reset requirement.
211211+- Bootstrap ordering: no auth/network client creation before provider setting load.
212212+- Routing epoch/version stale-response guard behavior.
213213+- Header injection (`atproto-proxy`) per request class.
214214+- Fallback state machine and circuit breaker behavior.
215215+- Capability matrix enforcement.
216216+217217+### Integration
218218+219219+- Signed-out profile/thread fetch through Bluesky and Blacksky.
220220+- Forced primary failure -> alternate provider fallback (when enabled).
221221+- Forced primary failure -> no cross-provider fallback (when disabled).
222222+- Constellation + Slingshot fallback success path.
223223+224224+### Regression
225225+226226+- Ensure `com.atproto.*` routes are unaffected.
227227+- Ensure OAuth/App Password auth flows still resolve correct PDS.
228228+229229+## Non-goals (for this phase)
230230+231231+- Supporting arbitrary third-party AppViews in UI without validation.
232232+- Automatic provider switching for write endpoints.
233233+- Replacing existing Constellation features.
234234+235235+## Sources
236236+237237+- <https://docs.bsky.app/docs/advanced-guides/api-directory>
238238+- <https://atproto.com/blog/2025-protocol-roadmap-spring>
239239+- <https://api.bsky.app/.well-known/did.json>
240240+- <https://api.blacksky.community/.well-known/did.json>
241241+- <https://docs.blacksky.community/list-of-our-services>
242242+- <https://www.microcosm.blue/>
243243+- <https://constellation.microcosm.blue/>
244244+- <https://slingshot.microcosm.blue/>
+56
docs/tasks/notification.md
···11+---
22+title: Notification Milestones
33+updated: 2026-04-29
44+---
55+66+## M1 - Foundation Hardening (Polling Baseline)
77+88+- [ ] Introduce `NotificationDomainService` orchestration layer
99+- [ ] Add Drift `notification_deliveries` table with migration
1010+- [ ] Route existing polling paths through orchestration layer
1111+- [ ] Add unit tests for dedupe and state persistence
1212+1313+## M2 - Local Notifications from Reconcile
1414+1515+- [ ] Add local notification adapter abstraction
1616+- [ ] Android channels by reason family
1717+- [ ] iOS category + payload deep-link mapping
1818+- [ ] Show local notifications for newly discovered unseen items
1919+- [ ] Add widget/integration tests for tap -> route behavior
2020+2121+## M3 - Push Registration Lifecycle
2222+2323+- [ ] Add token acquisition and refresh listeners
2424+- [ ] Implement `registerPush` and `unregisterPush`
2525+- [ ] Wire account login/switch/logout paths
2626+- [ ] Add retries/backoff for registration failures
2727+- [ ] Add unit tests for lifecycle transitions
2828+2929+## M4 - Push Payload Processing
3030+3131+- [ ] Add background payload entrypoint (`@pragma('vm:entry-point')`)
3232+- [ ] Parse defensively: `senderDid`, `targetDid`, `recordUri`, `reason`
3333+- [ ] Fetch canonical notification payload before display
3434+- [ ] Apply moderation + preference filtering before display
3535+- [ ] Add timeout-bound processing and drop accounting
3636+3737+## M5 - Background Reconciliation
3838+3939+- [ ] Add periodic background reconcile task (Android 15m+)
4040+- [ ] Add iOS background fetch/BGTaskScheduler integration
4141+- [ ] Ensure tasks are idempotent and dedupe-safe
4242+- [ ] Add test harness for worker entrypoints
4343+4444+## M6 - Preferences and UX
4545+4646+- [ ] Add settings UI for notification controls and permission state
4747+- [ ] Integrate `getPreferences` and `putPreferencesV2`
4848+- [ ] Add contextual permission prompt + denied -> settings flow
4949+- [ ] Add unread badge + seen-state reconciliation tests
5050+5151+## M7 - Reliability and Release Readiness
5252+5353+- [ ] Add structured logs + debug counters for notification flows
5454+- [ ] Add smoke checklist for Android/iOS permission and delivery scenarios
5555+- [ ] Validate multi-account behavior and token cleanup
5656+- [ ] Run full `flutter analyze` and full test suite
+57
docs/tasks/routing.md
···11+---
22+title: AppView Routing Milestones
33+updated: 2026-04-29
44+---
55+66+## M1 - Core Routing Model
77+88+- [ ] Add `appview_provider` setting with defaults and validation
99+- [ ] Add login-screen provider selector (Bluesky + Blacksky visible by default)
1010+- [ ] Persist login-screen provider choice before any auth/network call starts
1111+- [ ] Add provider descriptor model (`serviceDid`, `publicBaseUrl`, `entrywayUrl`)
1212+- [ ] Add `AppViewRouter` abstraction for endpoint/header resolution
1313+- [ ] Add unit tests for provider normalization/defaults and bootstrap ordering
1414+1515+## M2 - Header + Request Integration
1616+1717+- [ ] Inject explicit `atproto-proxy` for authenticated `app.bsky.*` requests
1818+- [ ] Route signed-out public `app.bsky.*` reads via selected provider host
1919+- [ ] Ensure `com.atproto.*` flows bypass AppView routing changes
2020+- [ ] Add integration tests for Bluesky/Blacksky provider selection
2121+2222+## M3 - Fallback Engine
2323+2424+- [ ] Add user setting for cross-provider fallback (default off)
2525+- [ ] Implement bounded fallback chain for read-only public endpoints
2626+- [ ] Add circuit-breaker window per provider/endpoint
2727+- [ ] Add structured logs for provider/fallback decisions
2828+- [ ] Add tests for timeout/429/5xx transitions with fallback enabled/disabled
2929+3030+## M4 - microcosm Fallbacks
3131+3232+- [ ] Keep Constellation fallback paths first-class for backlink-style enrichments
3333+- [ ] Add setting-gated Slingshot identity fallback for degraded handle resolution
3434+- [ ] Add tests for fallback parsing and failure handling
3535+- [ ] Document opt-in behavior and trust boundaries
3636+3737+## M5 - Settings and UX
3838+3939+- [ ] Add AppView provider controls in Settings (bluesky/blacksky)
4040+- [ ] Add provider-change confirmation that performs app reset
4141+- [ ] Define reset copy: stay signed in, no local data deletion, restart required
4242+- [ ] Show concise warning about moderation/ranking/provider differences
4343+- [ ] Add advanced diagnostics view (active provider, last fallback, last error)
4444+- [ ] Add manual "Refresh Provider Health" action
4545+4646+## M6 - Auth Flow Alignment
4747+4848+- [ ] Tie OAuth entryway default to selected provider (`bsky.social`/`blacksky.app`)
4949+- [ ] Validate app-password and OAuth flows remain backward compatible
5050+- [ ] Add migration behavior for existing saved sessions/accounts
5151+- [ ] Ensure app-level soft restart rebuilds DI/blocs/repositories after provider switch
5252+- [ ] Ensure stale pre-reset responses are ignored (routing epoch/version guard)
5353+5454+## M7 - Hardening and Release
5555+5656+- [ ] Add provider health probes at startup
5757+- [ ] Add capability matrix checks before retries