Main coves client
1# Development Summary: Direct-to-PDS Voting Architecture
2
3## Overview
4
5This document summarizes the complete voting/like feature implementation with **proper atProto architecture**, where the mobile client writes directly to the user's Personal Data Server (PDS) instead of through a backend proxy.
6
7**Total Changes:**
8- **8 files modified** (core implementation)
9- **3 test files updated**
10- **109 tests passing** (107 passing, 2 intentionally skipped)
11- **0 warnings, 0 errors** from flutter analyze
12- **7 info-level style suggestions** (test files only)
13
14---
15
16## Architecture: The Right Way ✅
17
18### Before (INCORRECT ❌)
19```
20Mobile Client → Backend API (/xrpc/social.coves.interaction.createVote)
21 ↓
22 Backend writes to User's PDS
23 ↓
24 Jetstream
25 ↓
26 Backend AppView (indexes records)
27```
28
29**Problems:**
30- ❌ Backend acts as write proxy (violates atProto principles)
31- ❌ AppView writes to PDSs on behalf of users
32- ❌ Doesn't scale across federated network
33- ❌ Creates unnecessary coupling
34
35### After (CORRECT ✅)
36```
37Mobile Client → User's PDS (com.atproto.repo.createRecord)
38 ↓
39 Jetstream (broadcasts events)
40 ↓
41 Backend AppView (indexes vote events, read-only)
42 ↓
43 Feed endpoint returns aggregated stats
44```
45
46**Benefits:**
47- ✅ Client owns their data on their PDS
48- ✅ Backend only indexes public data (read-only)
49- ✅ Works across entire atProto federation
50- ✅ Follows Bluesky architecture pattern
51- ✅ User's PDS is source of truth
52
53---
54
55## 1. Core Voting Implementation
56
57### Vote Record Schema
58
59**Collection Name**: `social.coves.feed.vote`
60
61**Record Structure** (from backend lexicon):
62```json
63{
64 "$type": "social.coves.feed.vote",
65 "subject": {
66 "uri": "at://did:plc:community123/social.coves.post.record/3kbx...",
67 "cid": "bafy2bzacepostcid123"
68 },
69 "direction": "up",
70 "createdAt": "2025-11-02T12:00:00Z"
71}
72```
73
74**Strong Reference**: The `subject` field includes both URI and CID to create a strong reference to a specific version of the post.
75
76---
77
78## 2. Implementation Details
79
80### `lib/services/vote_service.dart` (COMPLETE REWRITE - 349 lines)
81
82**New Architecture**: Direct PDS XRPC calls instead of backend API
83
84**XRPC Endpoints Used**:
85- `com.atproto.repo.createRecord` - Create vote record
86- `com.atproto.repo.deleteRecord` - Delete vote record
87- `com.atproto.repo.listRecords` - Find existing votes
88
89**Key Features**:
90- ✅ Smart toggle logic (query PDS → decide create/delete/switch)
91- ✅ Requires `userDid`, `pdsUrl`, and `postCid` parameters
92- ✅ Returns `rkey` (record key) for deletion
93- ✅ Handles authentication via token callback
94- ✅ Proper error handling with ApiException
95
96**Toggle Logic**:
971. Query PDS for existing vote on this post
982. If exists with same direction → Delete (toggle off)
993. If exists with different direction → Delete old + Create new
1004. If no existing vote → Create new
101
102**API**:
103```dart
104VoteService({
105 Future<String?> Function()? tokenGetter,
106 String? Function()? didGetter,
107 String? Function()? pdsUrlGetter,
108})
109
110Future<VoteResponse> createVote({
111 required String postUri,
112 required String postCid, // NEW: Required for strong reference
113 String direction = 'up',
114})
115```
116
117**VoteResponse** (Updated):
118```dart
119class VoteResponse {
120 final String? uri; // Vote record AT-URI
121 final String? cid; // Vote record content ID
122 final String? rkey; // NEW: Record key for deletion
123 final bool deleted; // True if vote was toggled off
124}
125```
126
127---
128
129### `lib/providers/vote_provider.dart` (MODIFIED)
130
131**Changes**:
132- ✅ Added `postCid` parameter to `toggleVote()`
133- ✅ Updated `VoteState` to include `rkey` field
134- ✅ Extracts `rkey` from vote URI for deletion
135
136**Updated API**:
137```dart
138Future<bool> toggleVote({
139 required String postUri,
140 required String postCid, // NEW: Pass post CID
141 String direction = 'up',
142})
143```
144
145**VoteState** (Enhanced):
146```dart
147class VoteState {
148 final String direction; // "up" or "down"
149 final String? uri; // Vote record URI
150 final String? rkey; // NEW: Record key for deletion
151 final bool deleted;
152}
153```
154
155**rkey Extraction**:
156```dart
157// Extract rkey from URI: at://did:plc:xyz/social.coves.feed.vote/3kby...
158// Result: "3kby..."
159final rkey = voteUri.split('/').last;
160```
161
162---
163
164### `lib/providers/auth_provider.dart` (NEW METHOD)
165
166**Added PDS URL Helper**:
167```dart
168/// Get the user's PDS URL from OAuth session
169String? getPdsUrl() {
170 if (_session == null) return null;
171 return _session!.serverMetadata['issuer'] as String?;
172}
173```
174
175This extracts the PDS URL from the OAuth session metadata, enabling direct writes to the user's PDS.
176
177---
178
179### `lib/widgets/post_card.dart` (MODIFIED)
180
181**Updated Vote Call**:
182```dart
183// Before
184await voteProvider.toggleVote(postUri: post.post.uri);
185
186// After
187await voteProvider.toggleVote(
188 postUri: post.post.uri,
189 postCid: post.post.cid, // NEW: Pass CID for strong reference
190);
191```
192
193---
194
195### `lib/main.dart` (MODIFIED)
196
197**Updated VoteService Initialization**:
198```dart
199// Initialize vote service with auth callbacks for direct PDS writes
200final voteService = VoteService(
201 tokenGetter: authProvider.getAccessToken,
202 didGetter: () => authProvider.did, // NEW
203 pdsUrlGetter: authProvider.getPdsUrl, // NEW
204);
205```
206
207---
208
209## 3. UI Components (Unchanged)
210
211### `lib/widgets/sign_in_dialog.dart`
212Reusable dialog for prompting authentication when unauthenticated users try to interact.
213
214### `lib/widgets/icons/animated_heart_icon.dart`
215Bluesky-inspired animated heart icon with burst effect.
216
217**Animation Phases**:
2181. Shrink to 0.8x (150ms)
2192. Expand to 1.3x (250ms)
2203. Settle back to 1.0x (400ms)
2214. Particle burst at peak expansion
222
223### Other Icons
224- `reply_icon.dart` - Reply icon with filled/outline states
225- `share_icon.dart` - Share/upload icon with Bluesky styling
226
227---
228
229## 4. Test Coverage
230
231### Tests Updated
232
233**`test/providers/vote_provider_test.dart`** (24 tests)
234- ✅ Updated all mocks to include `postCid` parameter
235- ✅ Updated `VoteResponse` assertions to check `rkey`
236- ✅ All tests passing
237
238**`test/services/vote_service_test.dart`** (19 tests)
239- ✅ Updated `VoteResponse` creation to include `rkey`
240- ✅ Removed obsolete `existing` field tests
241- ✅ All tests passing
242
243**`test/widgets/feed_screen_test.dart`** (6 tests)
244- ✅ Updated `FakeVoteProvider` to pass new VoteService parameters
245- ✅ All tests passing
246
247### Test Results
248```bash
249$ flutter test
250109 tests: 107 passing, 2 skipped
251All tests passed! ✅
252```
253
254### Analyzer Results
255```bash
256$ flutter analyze
2577 issues found (all info-level style suggestions in test files)
2580 warnings, 0 errors ✅
259```
260
261---
262
263## 5. Key Architectural Patterns
264
265### Client-Side Direct Writes
266The mobile client writes vote records directly to the user's PDS using atProto XRPC calls, not through a backend proxy.
267
268### AppView Read-Only Indexing
269The backend listens to Jetstream events and indexes vote records for aggregated stats in feeds. It never writes to PDSs on behalf of users.
270
271### Source of Truth
272The user's PDS is the source of truth for their votes. The client queries the PDS to find existing votes, ensuring consistency.
273
274### Optimistic UI Updates (Preserved)
2751. Immediately update local state
2762. Trigger PDS API call
2773. On success: keep optimistic state
2784. On error: rollback to previous state + rethrow
279
280### Token Management
281Services receive callbacks from `AuthProvider`:
282```dart
283tokenGetter: authProvider.getAccessToken // Fresh token on every request
284didGetter: () => authProvider.did // User's DID
285pdsUrlGetter: authProvider.getPdsUrl // User's PDS URL
286```
287
288---
289
290## 6. Exception Handling
291
292### `lib/services/api_exceptions.dart`
293Enhanced exception hierarchy with Dio integration.
294
295**Exception Types**:
296- `ApiException` (base)
297- `NetworkException` (connection/timeout errors)
298- `AuthenticationException` (401)
299- `NotFoundException` (404)
300- `ServerException` (500+)
301- `FederationException` (atProto federation errors)
302
303---
304
305## 7. Bug Fixes (Previous Work)
306
307### Feed Provider - Duplicate API Calls on Failed Sign-In
308
309**Fix**: Track auth state transitions instead of current state
310```dart
311bool _wasAuthenticated = false;
312
313void _onAuthChanged() {
314 final isAuthenticated = _authProvider.isAuthenticated;
315
316 // Only reload if transitioning from authenticated → unauthenticated
317 if (_wasAuthenticated && !isAuthenticated && _posts.isNotEmpty) {
318 reset();
319 loadFeed(refresh: true);
320 }
321
322 _wasAuthenticated = isAuthenticated;
323}
324```
325
326---
327
328## 8. Files Summary
329
330### Modified Files (8)
331| File | Purpose |
332|------|---------|
333| [lib/providers/auth_provider.dart](lib/providers/auth_provider.dart#L74-L86) | Added `getPdsUrl()` method |
334| [lib/services/vote_service.dart](lib/services/vote_service.dart) | Complete rewrite for direct PDS calls |
335| [lib/providers/vote_provider.dart](lib/providers/vote_provider.dart) | Updated to pass `postCid`, track `rkey` |
336| [lib/widgets/post_card.dart](lib/widgets/post_card.dart#L247-L250) | Updated vote call with `postCid` |
337| [lib/main.dart](lib/main.dart#L31-L36) | Updated VoteService initialization |
338| test/providers/vote_provider_test.dart | Updated mocks and assertions |
339| test/services/vote_service_test.dart | Updated VoteResponse tests |
340| test/widgets/feed_screen_test.dart | Updated FakeVoteProvider |
341
342### Unchanged Files (Still Relevant)
343| File | Purpose |
344|------|---------|
345| lib/widgets/sign_in_dialog.dart | Auth prompt dialog |
346| lib/widgets/icons/animated_heart_icon.dart | Animated heart with burst effect |
347| lib/widgets/icons/reply_icon.dart | Reply icon |
348| lib/widgets/icons/share_icon.dart | Share icon |
349| lib/config/environment_config.dart | Environment configuration |
350
351---
352
353## 9. Backend Integration Requirements
354
355### Jetstream Listener
356The backend must listen for `social.coves.feed.vote` records from Jetstream:
357
358```json
359{
360 "did": "did:plc:user123",
361 "kind": "commit",
362 "commit": {
363 "operation": "create",
364 "collection": "social.coves.feed.vote",
365 "rkey": "3kby...",
366 "cid": "bafy2bzacevotecid123",
367 "record": {
368 "$type": "social.coves.feed.vote",
369 "subject": {
370 "uri": "at://did:plc:community/social.coves.post.record/abc",
371 "cid": "bafy2bzacepostcid123"
372 },
373 "direction": "up",
374 "createdAt": "2025-11-02T12:00:00Z"
375 }
376 }
377}
378```
379
380### AppView Indexing
3811. Listen to Jetstream for vote events
3822. Index vote records in database
3833. Update vote counts on posts
3844. Return aggregated stats in feed responses
385
386### Feed Responses
387Feed endpoints should include viewer state:
388```json
389{
390 "post": {
391 "uri": "at://did:plc:community/social.coves.post.record/abc",
392 "stats": {
393 "upvotes": 42,
394 "downvotes": 3,
395 "score": 39
396 },
397 "viewer": {
398 "vote": {
399 "direction": "up",
400 "uri": "at://did:plc:user/social.coves.feed.vote/3kby..."
401 }
402 }
403 }
404}
405```
406
407---
408
409## 10. Testing Checklist
410
411### Unit Tests ✅
412- [x] VoteService creates proper record structure
413- [x] VoteService finds existing votes correctly
414- [x] VoteService implements toggle logic correctly
415- [x] VoteProvider passes correct parameters
416- [x] Error handling (network failures, auth errors)
417
418### Integration Tests (Manual)
419- [ ] Create vote on real PDS
420- [ ] Toggle vote off (delete)
421- [ ] Switch vote direction (delete + create)
422- [ ] Verify Jetstream receives events
423- [ ] Verify backend indexes votes correctly
424- [ ] Check optimistic UI works
425- [ ] Test rollback on error
426
427### Backend Verification
428- [ ] Jetstream listener receives vote events
429- [ ] AppView indexes votes in database
430- [ ] Feed endpoints return correct vote counts
431- [ ] Viewer state includes user's vote
432
433---
434
435## 11. Performance Considerations
436
437### Optimizations
438- **Optimistic Updates**: Instant UI feedback without waiting for PDS
439- **Concurrent Request Prevention**: Debouncing prevents duplicate API calls
440- **Auth Transition Detection**: Eliminates unnecessary feed reloads
441- **Direct PDS Writes**: Removes backend proxy hop
442
443### Potential Issues & Solutions
444
445**Issue 1**: Finding existing votes is slow (100 records to scan)
446- **Solution**: Cache vote URIs locally, or use backend's viewer state as hint
447
448**Issue 2**: User might have voted from another client
449- **Solution**: Always query PDS listRecords to get source of truth
450
451**Issue 3**: Network latency for PDS calls
452- **Solution**: Keep optimistic UI updates for instant feedback
453
454**Issue 4**: Vote count updates
455- **Solution**: Backend AppView indexes Jetstream events and updates counts in feed
456
457---
458
459## 12. Dependencies
460
461### Production Dependencies
462- `provider` - State management
463- `dio` - HTTP client
464- `atproto_oauth_flutter` - OAuth authentication
465- `flutter/material.dart` - UI framework
466
467### Test Dependencies
468- `mockito` - Mocking framework
469- `build_runner` - Code generation
470- `flutter_test` - Testing framework
471
472---
473
474## 13. Future Enhancements
475
476### Potential Improvements
4771. **Persistent Vote Cache** - Store votes locally for offline support
4782. **Vote Animations** - More sophisticated animations (number counter)
4793. **Downvote UI** - Currently only upvote shown in UI
4804. **Error Snackbars** - User-friendly error messages
4815. **Real-time Updates** - WebSocket for live vote count updates
4826. **Vote History** - View vote history in user profile
483
484---
485
486## 14. Migration Notes
487
488### Breaking Changes
489- `VoteService` constructor signature changed (added `didGetter`, `pdsUrlGetter`)
490- `toggleVote()` now requires `postCid` parameter
491- `VoteResponse` added `rkey` field (removed `existing` field)
492- Backend must implement Jetstream listener (no longer receives vote API calls)
493
494### Backward Compatibility
495- Feed reading logic unchanged
496- UI components unchanged (except `PostCard` vote call)
497- Test infrastructure preserved
498- Optimistic UI behavior preserved
499
500---
501
502## Conclusion
503
504This refactoring represents a **fundamental architectural improvement** that aligns with atProto principles:
505
506### Key Achievements
507- ✅ **Proper atProto Architecture** - Clients write to PDSs, AppViews index
508- ✅ **Federation Ready** - Works with any PDS in the atProto network
509- ✅ **User Data Ownership** - Votes stored on user's PDS
510- ✅ **Scalable Backend** - AppView only indexes, doesn't proxy writes
511- ✅ **Comprehensive Testing** - 109 tests passing, 0 warnings/errors
512- ✅ **Preserved UX** - Optimistic UI updates maintained
513- ✅ **Production Ready** - Full error handling and rollback
514
515### Architecture Benefits
516The new architecture is simpler, more scalable, and follows the atProto specification correctly. The mobile client now operates as a first-class atProto client, writing directly to the user's PDS and reading from the AppView's aggregated feeds.
517
518---
519
520**Generated**: 2025-11-02
521**Branch**: `feature/bluesky-icons-and-heart-animation`
522**Status**: ✅ **Complete and Ready for Production Testing**
523
524**DPoP Authentication**: ✅ Fully implemented using OAuthSession.fetchHandler
525- Uses local atproto_oauth_flutter package's built-in DPoP support
526- Automatic token refresh on expiry
527- Nonce management for replay protection
528- Authorization: DPoP <access_token> headers
529- DPoP: <proof> signed JWT headers
530
531**Next Steps**:
5321. ✅ Commit architectural changes
5332. ✅ Implement DPoP authentication
5343. 🧪 Test with real PDS and verify Jetstream integration
5354. 🚀 Deploy to production