···1717 - Dev mode -- show did, copy did in profiles (remove "logged in as <did>" from home page)
1818 - Toggle for table view vs future post-style view
19192020-## Fixes
2020+## Far Future Considerations
2121+2222+- Consider fully separating API backend from frontend service
2323+ - Currently using HTMX header checks to prevent direct browser access to internal API endpoints
2424+ - If adding mobile apps, third-party API consumers, or microservices architecture, revisit this
2525+ - For now, monolithic approach is appropriate for HTMX-based web app with decentralized storage
21262222-- Loading columns for brews table doesn't match loaded column names
2727+## Fixes
+314
docs/jetstream-tap-evaluation.md
···11+# Jetstream and Tap Evaluation for Arabica
22+33+## Executive Summary
44+55+This document evaluates two AT Protocol synchronization tools - **Jetstream** and **Tap** - for potential integration with Arabica. These tools could help reduce API requests for the community feed feature and simplify real-time data synchronization.
66+77+**Recommendation:** Consider Jetstream for community feed improvements in the near term; Tap is overkill for Arabica's current scale but valuable for future growth.
88+99+---
1010+1111+## Background: Current Arabica Architecture
1212+1313+Arabica currently interacts with AT Protocol in two ways:
1414+1515+1. **Authenticated User Operations** (`internal/atproto/store.go`)
1616+ - Direct XRPC calls to user's PDS for CRUD operations
1717+ - Per-session in-memory cache (5-minute TTL)
1818+ - Each user's data stored in their own PDS
1919+2020+2. **Community Feed** (`internal/feed/service.go`)
2121+ - Polls registered users' PDSes to aggregate recent activity
2222+ - Fetches profiles, brews, beans, roasters, grinders, brewers from each user
2323+ - Public feed cached for 5 minutes
2424+ - **Problem:** N+1 query pattern - each registered user requires multiple API calls
2525+2626+### Current Feed Inefficiency
2727+2828+For N registered users, the feed service makes approximately:
2929+- N profile fetches
3030+- N x 5 collection fetches (brew, bean, roaster, grinder, brewer) for recent items
3131+- N x 4 collection fetches for reference resolution
3232+- **Total: ~10N API calls per feed refresh**
3333+3434+---
3535+3636+## Tool 1: Jetstream
3737+3838+### What It Is
3939+4040+Jetstream is a streaming service that consumes the AT Protocol firehose (`com.atproto.sync.subscribeRepos`) and converts it into lightweight JSON events. It's operated by Bluesky at public endpoints.
4141+4242+**Public Instances:**
4343+- `jetstream1.us-east.bsky.network`
4444+- `jetstream2.us-east.bsky.network`
4545+- `jetstream1.us-west.bsky.network`
4646+- `jetstream2.us-west.bsky.network`
4747+4848+### Key Features
4949+5050+| Feature | Description |
5151+|---------|-------------|
5252+| JSON Output | Simple JSON instead of CBOR/CAR binary encoding |
5353+| Filtering | Filter by collection (NSID) or repo (DID) |
5454+| Compression | ~56% smaller messages with zstd compression |
5555+| Low Latency | Real-time event delivery |
5656+| Easy to Use | Standard WebSocket connection |
5757+5858+### Jetstream Event Example
5959+6060+```json
6161+{
6262+ "did": "did:plc:eygmaihciaxprqvxpfvl6flk",
6363+ "time_us": 1725911162329308,
6464+ "kind": "commit",
6565+ "commit": {
6666+ "rev": "3l3qo2vutsw2b",
6767+ "operation": "create",
6868+ "collection": "social.arabica.alpha.brew",
6969+ "rkey": "3l3qo2vuowo2b",
7070+ "record": {
7171+ "$type": "social.arabica.alpha.brew",
7272+ "method": "pourover",
7373+ "rating": 4,
7474+ "createdAt": "2024-09-09T19:46:02.102Z"
7575+ },
7676+ "cid": "bafyreidwaivazkwu67xztlmuobx35hs2lnfh3kolmgfmucldvhd3sgzcqi"
7777+ }
7878+}
7979+```
8080+8181+### How Arabica Could Use Jetstream
8282+8383+**Use Case: Real-time Community Feed**
8484+8585+Instead of polling each user's PDS every 5 minutes, Arabica could:
8686+8787+1. Subscribe to Jetstream filtered by:
8888+ - `wantedCollections`: `social.arabica.alpha.*`
8989+ - `wantedDids`: List of registered feed users
9090+9191+2. Maintain a local feed index updated in real-time
9292+9393+3. Serve feed directly from local index (instant response, no API calls)
9494+9595+**Implementation Sketch:**
9696+9797+```go
9898+// Subscribe to Jetstream for Arabica collections
9999+ws, _ := websocket.Dial("wss://jetstream1.us-east.bsky.network/subscribe?" +
100100+ "wantedCollections=social.arabica.alpha.brew&" +
101101+ "wantedCollections=social.arabica.alpha.bean&" +
102102+ "wantedDids=" + strings.Join(registeredDids, "&wantedDids="))
103103+104104+// Process events in background goroutine
105105+for {
106106+ var event JetstreamEvent
107107+ ws.ReadJSON(&event)
108108+109109+ switch event.Commit.Collection {
110110+ case "social.arabica.alpha.brew":
111111+ feedIndex.AddBrew(event.DID, event.Commit.Record)
112112+ case "social.arabica.alpha.bean":
113113+ feedIndex.AddBean(event.DID, event.Commit.Record)
114114+ }
115115+}
116116+```
117117+118118+### Jetstream Tradeoffs
119119+120120+| Pros | Cons |
121121+|------|------|
122122+| Dramatically reduces API calls | No cryptographic verification of data |
123123+| Real-time updates (sub-second latency) | Requires persistent WebSocket connection |
124124+| Simple JSON format | Trust relationship with Jetstream operator |
125125+| Can filter by collection/DID | Not part of formal AT Protocol spec |
126126+| Free public instances available | No built-in backfill mechanism |
127127+128128+### Jetstream Verdict for Arabica
129129+130130+**Recommended for:** Community feed real-time updates
131131+132132+**Not suitable for:** Authenticated user operations (those need direct PDS calls)
133133+134134+**Effort estimate:** Medium (1-2 weeks)
135135+- Add WebSocket client for Jetstream
136136+- Build local feed index (could use BoltDB or in-memory)
137137+- Handle reconnection/cursor management
138138+- Still need initial backfill via direct API
139139+140140+---
141141+142142+## Tool 2: Tap
143143+144144+### What It Is
145145+146146+Tap is a synchronization tool for AT Protocol that handles the complexity of repo synchronization. It subscribes to a Relay and outputs filtered, verified events. Tap is more comprehensive than Jetstream but requires running your own instance.
147147+148148+**Repository:** `github.com/bluesky-social/indigo/cmd/tap`
149149+150150+### Key Features
151151+152152+| Feature | Description |
153153+|---------|-------------|
154154+| Automatic Backfill | Fetches complete history when tracking new repos |
155155+| Verification | MST integrity checks, signature validation |
156156+| Recovery | Auto-resyncs if repo becomes desynchronized |
157157+| Flexible Delivery | WebSocket, fire-and-forget, or webhooks |
158158+| Filtered Output | DID and collection filtering |
159159+160160+### Tap Operating Modes
161161+162162+1. **Dynamic (default):** Add DIDs via API as needed
163163+2. **Collection Signal:** Auto-track repos with records in specified collection
164164+3. **Full Network:** Mirror entire AT Protocol network (resource-intensive)
165165+166166+### How Arabica Could Use Tap
167167+168168+**Use Case: Complete Feed Infrastructure**
169169+170170+Tap could replace the entire feed polling mechanism:
171171+172172+1. Run Tap instance with `TAP_SIGNAL_COLLECTION=social.arabica.alpha.brew`
173173+2. Tap automatically discovers and tracks users who create brew records
174174+3. Feed service consumes events from local Tap instance
175175+4. No manual user registration needed - Tap discovers users automatically
176176+177177+**Collection Signal Mode:**
178178+179179+```bash
180180+# Start Tap to auto-track repos with Arabica records
181181+TAP_SIGNAL_COLLECTION=social.arabica.alpha.brew \
182182+ go run ./cmd/tap --disable-acks=true
183183+```
184184+185185+**Webhook Delivery (Serverless-friendly):**
186186+187187+Tap can POST events to an HTTP endpoint, making it compatible with serverless architectures:
188188+189189+```bash
190190+# Tap sends events to Arabica webhook
191191+TAP_WEBHOOK_URL=https://arabica.example/api/feed-webhook \
192192+ go run ./cmd/tap
193193+```
194194+195195+### Tap Tradeoffs
196196+197197+| Pros | Cons |
198198+|------|------|
199199+| Automatic backfill when adding repos | Requires running your own service |
200200+| Full cryptographic verification | More operational complexity |
201201+| Handles cursor management | Resource requirements (DB, network) |
202202+| Auto-discovers users via collection signal | Overkill for small user bases |
203203+| Webhook support for serverless | Still in beta |
204204+205205+### Tap Verdict for Arabica
206206+207207+**Recommended for:** Future growth when feed has many users
208208+209209+**Not suitable for:** Current scale (< 100 registered users)
210210+211211+**Effort estimate:** High (2-4 weeks)
212212+- Deploy and operate Tap service
213213+- Integrate webhook or WebSocket consumer
214214+- Migrate feed service to consume from Tap
215215+- Handle Tap service reliability/monitoring
216216+217217+---
218218+219219+## Comparison Matrix
220220+221221+| Aspect | Current Polling | Jetstream | Tap |
222222+|--------|----------------|-----------|-----|
223223+| API Calls per Refresh | ~10N | 0 (after connection) | 0 (after backfill) |
224224+| Latency | 5 min cache | Real-time | Real-time |
225225+| Backfill | Full fetch each time | Manual | Automatic |
226226+| Verification | Trusts PDS | Trusts Jetstream | Full verification |
227227+| Operational Cost | None | None (public) | Run own service |
228228+| Complexity | Low | Medium | High |
229229+| User Discovery | Manual registry | Manual | Auto via collection |
230230+| Recommended Scale | < 50 users | 50-1000 users | 1000+ users |
231231+232232+---
233233+234234+## Recommendation
235235+236236+### Short Term (Now - 6 months)
237237+238238+**Stick with current polling + caching approach**
239239+240240+Rationale:
241241+- Current implementation works
242242+- User base is small
243243+- Polling N users with caching is acceptable
244244+245245+**Consider adding Jetstream for feed** if:
246246+- Feed latency becomes user-visible issue
247247+- Registered users exceed ~50
248248+- API rate limiting becomes a problem
249249+250250+### Medium Term (6-12 months)
251251+252252+**Implement Jetstream integration**
253253+254254+1. Add background Jetstream consumer
255255+2. Build local feed index (BoltDB or SQLite)
256256+3. Serve feed from local index
257257+4. Keep polling as fallback for backfill
258258+259259+### Long Term (12+ months)
260260+261261+**Evaluate Tap when:**
262262+- User base exceeds 500+ registered users
263263+- Want automatic user discovery
264264+- Need cryptographic verification for social features (likes, comments)
265265+- Building moderation/anti-abuse features
266266+267267+---
268268+269269+## Implementation Notes
270270+271271+### Jetstream Client Library
272272+273273+Bluesky provides a Go client library:
274274+275275+```go
276276+import "github.com/bluesky-social/jetstream/pkg/client"
277277+```
278278+279279+### Tap TypeScript Library
280280+281281+For frontend integration:
282282+283283+```typescript
284284+import { TapClient } from '@atproto/tap';
285285+```
286286+287287+### Connection Resilience
288288+289289+Both tools require handling:
290290+- WebSocket reconnection
291291+- Cursor persistence across restarts
292292+- Backpressure when events arrive faster than processing
293293+294294+### Caching Integration
295295+296296+Can coexist with current `SessionCache`:
297297+- Jetstream/Tap updates the local index
298298+- Local index serves feed requests
299299+- SessionCache continues for authenticated user operations
300300+301301+---
302302+303303+## Related Documentation
304304+305305+- Jetstream GitHub: https://github.com/bluesky-social/jetstream
306306+- Tap README: https://github.com/bluesky-social/indigo/blob/main/cmd/tap/README.md
307307+- Jetstream Blog Post: https://docs.bsky.app/blog/jetstream
308308+- Tap Blog Post: https://docs.bsky.app/blog/introducing-tap
309309+310310+---
311311+312312+## Note on "Constellation" and "Slingshot"
313313+314314+These terms don't appear to correspond to official AT Protocol tools as of this evaluation. If these refer to specific community projects or internal codenames, please provide additional context for evaluation.
+505
docs/microcosm-tools-evaluation.md
···11+# Microcosm Tools Evaluation for Arabica
22+33+## Executive Summary
44+55+This document evaluates three community-built AT Protocol infrastructure tools from [microcosm.blue](https://microcosm.blue/) - **Constellation**, **Spacedust**, and **Slingshot** - for potential integration with Arabica's community feed feature.
66+77+**Recommendation:** Adopt Constellation immediately for future social features (likes/comments). Consider Slingshot as an optional optimization for feed performance. Spacedust is ideal for real-time notifications when social features are implemented.
88+99+---
1010+1111+## Background: Current Arabica Architecture
1212+1313+### The Problem
1414+1515+Arabica's community feed (`internal/feed/service.go`) currently polls each registered user's PDS directly. For N registered users:
1616+1717+| API Call Type | Count per Refresh |
1818+|---------------|-------------------|
1919+| Profile fetches | N |
2020+| Brew collections | N |
2121+| Bean collections | N |
2222+| Roaster collections | N |
2323+| Grinder collections | N |
2424+| Brewer collections | N |
2525+| Reference resolution | ~4N |
2626+| **Total** | **~10N API calls** |
2727+2828+This approach has several issues:
2929+- **Latency**: Feed refresh is slow with many users
3030+- **Rate limits**: Risk of PDS rate limiting
3131+- **Reliability**: Feed fails if any PDS is slow/down
3232+- **Scalability**: Linear growth in API calls per user
3333+3434+### Future Social Features
3535+3636+Arabica plans to add likes, comments, and follows (see `AGENTS.md`). These interactions require **backlink queries** - given a brew, find all likes pointing at it. This is impossible with current polling approach.
3737+3838+---
3939+4040+## Tool 1: Constellation (Backlink Index)
4141+4242+### What It Is
4343+4444+Constellation is a **global backlink index** that crawls every record in the AT Protocol firehose and indexes all links (AT-URIs, DIDs, URLs). It answers "who/what points at this target?" queries.
4545+4646+**Public Instance:** `https://constellation.microcosm.blue`
4747+4848+### Key Capabilities
4949+5050+| Feature | Description |
5151+|---------|-------------|
5252+| Backlink queries | Find all records linking to a target |
5353+| Like/follow counts | Get interaction counts instantly |
5454+| Any lexicon support | Works with `social.arabica.alpha.*` |
5555+| DID filtering | Filter links by specific users |
5656+| Distinct DID counts | Count unique users, not just records |
5757+5858+### API Examples
5959+6060+**Get like count for a brew:**
6161+```bash
6262+curl "https://constellation.microcosm.blue/links/count/distinct-dids" \
6363+ -G --data-urlencode "target=at://did:plc:xxx/social.arabica.alpha.brew/abc123" \
6464+ --data-urlencode "collection=social.arabica.alpha.like" \
6565+ --data-urlencode "path=.subject.uri"
6666+```
6767+6868+**Get all users who liked a brew:**
6969+```bash
7070+curl "https://constellation.microcosm.blue/links/distinct-dids" \
7171+ -G --data-urlencode "target=at://did:plc:xxx/social.arabica.alpha.brew/abc123" \
7272+ --data-urlencode "collection=social.arabica.alpha.like" \
7373+ --data-urlencode "path=.subject.uri"
7474+```
7575+7676+**Get all comments on a brew:**
7777+```bash
7878+curl "https://constellation.microcosm.blue/links" \
7979+ -G --data-urlencode "target=at://did:plc:xxx/social.arabica.alpha.brew/abc123" \
8080+ --data-urlencode "collection=social.arabica.alpha.comment" \
8181+ --data-urlencode "path=.subject.uri"
8282+```
8383+8484+### How Arabica Could Use Constellation
8585+8686+**Use Case 1: Social Interaction Counts**
8787+8888+When displaying a brew in the feed, fetch interaction counts:
8989+9090+```go
9191+// Get like count for a brew
9292+func (c *ConstellationClient) GetLikeCount(ctx context.Context, brewURI string) (int, error) {
9393+ url := fmt.Sprintf("%s/links/count/distinct-dids?target=%s&collection=%s&path=%s",
9494+ c.baseURL,
9595+ url.QueryEscape(brewURI),
9696+ "social.arabica.alpha.like",
9797+ url.QueryEscape(".subject.uri"))
9898+9999+ // Returns {"total": 42}
100100+ var result struct { Total int `json:"total"` }
101101+ // ... fetch and decode
102102+ return result.Total, nil
103103+}
104104+```
105105+106106+**Use Case 2: Comment Threads**
107107+108108+Fetch all comments for a brew detail page:
109109+110110+```go
111111+func (c *ConstellationClient) GetComments(ctx context.Context, brewURI string) ([]Comment, error) {
112112+ // Constellation returns the AT-URIs of comment records
113113+ // Then fetch each comment from Slingshot or user's PDS
114114+}
115115+```
116116+117117+**Use Case 3: "Who liked this" List**
118118+119119+```go
120120+func (c *ConstellationClient) GetLikers(ctx context.Context, brewURI string) ([]string, error) {
121121+ // Returns list of DIDs who liked this brew
122122+ // Can hydrate with profile info from Slingshot
123123+}
124124+```
125125+126126+### Constellation Tradeoffs
127127+128128+| Pros | Cons |
129129+|------|------|
130130+| Instant interaction counts (no polling) | Third-party dependency |
131131+| Works with any lexicon including Arabica's | Not self-hosted (yet) |
132132+| Handles likes from any PDS globally | Slight index delay (~seconds) |
133133+| 11B+ links indexed, production-ready | Trusts Constellation operator |
134134+| Free public instance | Query limits may apply |
135135+136136+### Constellation Verdict
137137+138138+**Essential for:** Social features (likes, comments, follows)
139139+140140+**Not needed for:** Current feed polling (Constellation indexes interactions, not record listings)
141141+142142+**Effort estimate:** Low (1 week)
143143+- Add HTTP client for Constellation API
144144+- Integrate counts into brew display
145145+- Cache counts locally (5-minute TTL)
146146+147147+---
148148+149149+## Tool 2: Spacedust (Interactions Firehose)
150150+151151+### What It Is
152152+153153+Spacedust extracts **links** from every record in the AT Protocol firehose and re-emits them over WebSocket. Unlike Jetstream (which emits full records), Spacedust emits just the link relationships.
154154+155155+**Public Instance:** `wss://spacedust.microcosm.blue`
156156+157157+### Key Capabilities
158158+159159+| Feature | Description |
160160+|---------|-------------|
161161+| Real-time link events | Instantly know when someone likes/follows |
162162+| Filter by source/target | Subscribe to specific collections or targets |
163163+| Any lexicon support | Works with `social.arabica.alpha.*` |
164164+| Lightweight | Just links, not full records |
165165+166166+### Example: Subscribe to Likes on Your Brews
167167+168168+```javascript
169169+// WebSocket connection to Spacedust
170170+const ws = new WebSocket(
171171+ "wss://spacedust.microcosm.blue/subscribe" +
172172+ "?wantedSources=social.arabica.alpha.like:subject.uri" +
173173+ "&wantedSubjects=did:plc:your-did"
174174+);
175175+176176+ws.onmessage = (event) => {
177177+ const link = JSON.parse(event.data);
178178+ // { source: "at://...", target: "at://...", ... }
179179+ console.log("Someone liked your brew!");
180180+};
181181+```
182182+183183+### How Arabica Could Use Spacedust
184184+185185+**Use Case: Real-time Notifications**
186186+187187+When social features are added, Spacedust enables instant notifications:
188188+189189+```go
190190+// Background goroutine subscribes to Spacedust
191191+func (s *NotificationService) subscribeToInteractions(userDID string) {
192192+ ws := dial("wss://spacedust.microcosm.blue/subscribe" +
193193+ "?wantedSources=social.arabica.alpha.like:subject.uri" +
194194+ "&wantedSubjects=" + userDID)
195195+196196+ for {
197197+ link := readLink(ws)
198198+ // Someone liked a brew by userDID
199199+ s.notify(userDID, "Someone liked your brew!")
200200+ }
201201+}
202202+```
203203+204204+**Use Case: Live Feed Updates**
205205+206206+Push new brews to connected clients without polling:
207207+208208+```go
209209+// Subscribe to all Arabica brew creations
210210+ws := dial("wss://spacedust.microcosm.blue/subscribe" +
211211+ "?wantedSources=social.arabica.alpha.brew:beanRef")
212212+213213+// When a link event arrives, a new brew was created
214214+// Fetch full record from Slingshot and push to feed
215215+```
216216+217217+### Spacedust Tradeoffs
218218+219219+| Pros | Cons |
220220+|------|------|
221221+| Real-time, sub-second latency | Requires persistent WebSocket |
222222+| Lightweight link-only events | Still in v0 (missing some features) |
223223+| Filter by collection/target | No cursor replay yet |
224224+| Perfect for notifications | Need to hydrate records separately |
225225+226226+### Spacedust Verdict
227227+228228+**Ideal for:** Real-time notifications, live feed updates
229229+230230+**Not suitable for:** Current feed needs (need full records, not just links)
231231+232232+**Effort estimate:** Medium (2-3 weeks)
233233+- WebSocket client with reconnection
234234+- Notification service for social interactions
235235+- Integration with frontend for live updates
236236+- Depends on social features being implemented first
237237+238238+---
239239+240240+## Tool 3: Slingshot (Records & Identities Cache)
241241+242242+### What It Is
243243+244244+Slingshot is an **edge cache** for AT Protocol records and identities. It pre-caches records from the firehose and provides fast, authenticated access. Also resolves handles to DIDs with bi-directional verification.
245245+246246+**Public Instance:** `https://slingshot.microcosm.blue`
247247+248248+### Key Capabilities
249249+250250+| Feature | Description |
251251+|---------|-------------|
252252+| Fast record fetching | Pre-cached from firehose |
253253+| Identity resolution | `resolveMiniDoc` for handle/DID |
254254+| Bi-directional verification | Only returns verified handles |
255255+| Works with slow PDS | Cache serves even if PDS is down |
256256+| Standard XRPC API | Drop-in replacement for PDS calls |
257257+258258+### API Examples
259259+260260+**Resolve identity:**
261261+```bash
262262+curl "https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=bad-example.com"
263263+# Returns: { "did": "did:plc:...", "handle": "bad-example.com", "pds": "https://..." }
264264+```
265265+266266+**Get record (standard XRPC):**
267267+```bash
268268+curl "https://slingshot.microcosm.blue/xrpc/com.atproto.repo.getRecord?repo=did:plc:xxx&collection=social.arabica.alpha.brew&rkey=abc123"
269269+```
270270+271271+**List records:**
272272+```bash
273273+curl "https://slingshot.microcosm.blue/xrpc/com.atproto.repo.listRecords?repo=did:plc:xxx&collection=social.arabica.alpha.brew&limit=10"
274274+```
275275+276276+### How Arabica Could Use Slingshot
277277+278278+**Use Case 1: Faster Feed Fetching**
279279+280280+Replace direct PDS calls with Slingshot for public data:
281281+282282+```go
283283+// Before: Each user's PDS
284284+pdsEndpoint, _ := c.GetPDSEndpoint(ctx, did)
285285+url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords...", pdsEndpoint)
286286+287287+// After: Single Slingshot endpoint
288288+url := fmt.Sprintf("https://slingshot.microcosm.blue/xrpc/com.atproto.repo.listRecords...")
289289+```
290290+291291+**Benefits:**
292292+- Eliminates N DNS lookups for N user PDS endpoints
293293+- Single, fast endpoint for all public record fetches
294294+- Continues working even if individual PDS is slow/down
295295+- Pre-cached records = faster response times
296296+297297+**Use Case 2: Identity Resolution**
298298+299299+Replace multiple API calls with single `resolveMiniDoc`:
300300+301301+```go
302302+// Before: Two calls
303303+handle := resolveHandle(did) // Call 1
304304+pds := resolvePDSEndpoint(did) // Call 2
305305+306306+// After: One call
307307+mini := resolveMiniDoc(did)
308308+// { handle: "user.bsky.social", pds: "https://...", did: "did:plc:..." }
309309+```
310310+311311+**Use Case 3: Hydrate Records from Constellation**
312312+313313+When Constellation returns AT-URIs (e.g., comments on a brew), fetch the actual records from Slingshot:
314314+315315+```go
316316+// Constellation returns: ["at://did:plc:a/social.arabica.alpha.comment/123", ...]
317317+commentURIs := constellation.GetComments(ctx, brewURI)
318318+319319+// Fetch each comment record from Slingshot
320320+for _, uri := range commentURIs {
321321+ record := slingshot.GetRecord(ctx, uri)
322322+ // ...
323323+}
324324+```
325325+326326+### Implementation: Slingshot-Backed PublicClient
327327+328328+```go
329329+// internal/atproto/slingshot_client.go
330330+331331+const SlingshotBaseURL = "https://slingshot.microcosm.blue"
332332+333333+type SlingshotClient struct {
334334+ baseURL string
335335+ httpClient *http.Client
336336+}
337337+338338+func NewSlingshotClient() *SlingshotClient {
339339+ return &SlingshotClient{
340340+ baseURL: SlingshotBaseURL,
341341+ httpClient: &http.Client{Timeout: 10 * time.Second},
342342+ }
343343+}
344344+345345+// ListRecords uses Slingshot instead of user's PDS
346346+func (c *SlingshotClient) ListRecords(ctx context.Context, did, collection string, limit int) (*PublicListRecordsOutput, error) {
347347+ // Same XRPC API, different endpoint
348348+ url := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords?repo=%s&collection=%s&limit=%d&reverse=true",
349349+ c.baseURL, url.QueryEscape(did), url.QueryEscape(collection), limit)
350350+ // ... standard HTTP request
351351+}
352352+353353+// ResolveMiniDoc gets handle + PDS in one call
354354+func (c *SlingshotClient) ResolveMiniDoc(ctx context.Context, identifier string) (*MiniDoc, error) {
355355+ url := fmt.Sprintf("%s/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=%s",
356356+ c.baseURL, url.QueryEscape(identifier))
357357+ // ... returns { did, handle, pds }
358358+}
359359+```
360360+361361+### Slingshot Tradeoffs
362362+363363+| Pros | Cons |
364364+|------|------|
365365+| Faster than direct PDS calls | Third-party dependency |
366366+| Single endpoint for all users | May not have custom lexicons cached |
367367+| Identity verification built-in | Not all XRPC APIs implemented |
368368+| Resilient to slow/down PDS | Trusts Slingshot operator |
369369+| Pre-cached from firehose | Still in v0, some features missing |
370370+371371+### Slingshot Verdict
372372+373373+**Recommended for:** Feed performance optimization, identity resolution
374374+375375+**Not suitable for:** Authenticated user operations (still need direct PDS)
376376+377377+**Effort estimate:** Low (3-5 days)
378378+- Add SlingshotClient as optional PublicClient backend
379379+- Feature flag to toggle between direct PDS and Slingshot
380380+- Test with Arabica collections to ensure they're indexed
381381+382382+---
383383+384384+## Comparison: Current vs. Microcosm Tools
385385+386386+| Aspect | Current Polling | + Slingshot | + Constellation | + Spacedust |
387387+|--------|-----------------|-------------|-----------------|-------------|
388388+| Feed refresh latency | Slow (N PDS calls) | Fast (1 endpoint) | N/A | Real-time |
389389+| Like/comment counts | Impossible | Impossible | Instant | N/A |
390390+| Rate limit risk | High | Low | Low | None |
391391+| PDS failure resilience | Poor | Good | N/A | N/A |
392392+| Real-time updates | No (5min cache) | No | No | Yes |
393393+| Effort to integrate | N/A | Low | Low | Medium |
394394+395395+---
396396+397397+## Recommendation
398398+399399+### Immediate (Social Features Prerequisite)
400400+401401+**1. Integrate Constellation when adding likes/comments**
402402+403403+Constellation is essential for social features. When a brew is displayed, use Constellation to:
404404+- Show like count
405405+- Show comment count
406406+- Power "who liked this" lists
407407+- Power comment threads
408408+409409+**Implementation priority:** Do this alongside `social.arabica.alpha.like` and `social.arabica.alpha.comment` lexicon implementation.
410410+411411+### Short Term (Performance Optimization)
412412+413413+**2. Evaluate Slingshot for feed performance**
414414+415415+If feed latency becomes an issue:
416416+- Add SlingshotClient as alternative to direct PDS calls
417417+- A/B test performance improvement
418418+- Use for public record fetches only (keep direct PDS for authenticated writes)
419419+420420+**Trigger:** When registered users exceed ~20-30, or feed refresh exceeds 5 seconds
421421+422422+### Medium Term (Real-time Features)
423423+424424+**3. Add Spacedust for notifications**
425425+426426+When social features are live and users want notifications:
427427+- Subscribe to Spacedust for likes/comments on user's content
428428+- Push notifications via WebSocket to connected clients
429429+- Optional: background job for email notifications
430430+431431+**Trigger:** After social features launch, when users request notifications
432432+433433+---
434434+435435+## Comparison with Official Tools (Jetstream/Tap)
436436+437437+See `jetstream-tap-evaluation.md` for official Bluesky tools. Key differences:
438438+439439+| Aspect | Microcosm Tools | Official Tools |
440440+|--------|-----------------|----------------|
441441+| Focus | Links/interactions | Full records |
442442+| Backlink queries | Constellation (yes) | Not available |
443443+| Record caching | Slingshot | Not available |
444444+| Real-time | Spacedust (links) | Jetstream (records) |
445445+| Self-hosting | Not yet documented | Available |
446446+| Community | Community-supported | Bluesky-supported |
447447+448448+**Recommendation:** Use Microcosm tools for social features (likes/comments/follows) where backlink queries are essential. Consider Jetstream for full feed real-time if needed later.
449449+450450+---
451451+452452+## Implementation Plan
453453+454454+### Phase 1: Constellation Integration (with social features)
455455+456456+```
457457+1. Create internal/atproto/constellation.go
458458+ - ConstellationClient with HTTP client
459459+ - GetBacklinks(), GetLinkCount(), GetDistinctDIDs()
460460+461461+2. Create internal/social/interactions.go
462462+ - GetBrewLikeCount(brewURI)
463463+ - GetBrewComments(brewURI)
464464+ - GetBrewLikers(brewURI)
465465+466466+3. Update templates to show interaction counts
467467+ - Modify feed item display
468468+ - Add like button (when like lexicon ready)
469469+```
470470+471471+### Phase 2: Slingshot Optimization (optional)
472472+473473+```
474474+1. Create internal/atproto/slingshot.go
475475+ - SlingshotClient implementing same interface as PublicClient
476476+477477+2. Add feature flag: ARABICA_USE_SLINGSHOT=true
478478+479479+3. Modify feed/service.go to use SlingshotClient
480480+ - Keep PublicClient as fallback
481481+```
482482+483483+### Phase 3: Spacedust Notifications (future)
484484+485485+```
486486+1. Create internal/notifications/spacedust.go
487487+ - WebSocket client with reconnection
488488+ - Subscribe to user's content interactions
489489+490490+2. Create notification storage (BoltDB)
491491+492492+3. Add /api/notifications endpoint for frontend polling
493493+494494+4. Optional: WebSocket to frontend for real-time
495495+```
496496+497497+---
498498+499499+## Related Documentation
500500+501501+- Microcosm Main: https://microcosm.blue/
502502+- Constellation API: https://constellation.microcosm.blue/
503503+- Source Code: https://github.com/at-microcosm/microcosm-rs
504504+- Discord: https://discord.gg/tcDfe4PGVB
505505+- See also: `jetstream-tap-evaluation.md` for official Bluesky tools
+13
internal/bff/helpers.go
···233233234234 return websiteURL
235235}
236236+237237+// EscapeJS escapes a string for safe use in JavaScript string literals.
238238+// Handles newlines, quotes, backslashes, and other special characters.
239239+func EscapeJS(s string) string {
240240+ // Replace special characters that would break JavaScript strings
241241+ s = strings.ReplaceAll(s, "\\", "\\\\") // Must be first
242242+ s = strings.ReplaceAll(s, "'", "\\'")
243243+ s = strings.ReplaceAll(s, "\"", "\\\"")
244244+ s = strings.ReplaceAll(s, "\n", "\\n")
245245+ s = strings.ReplaceAll(s, "\r", "\\r")
246246+ s = strings.ReplaceAll(s, "\t", "\\t")
247247+ return s
248248+}
···176176177177// getClientIP is defined in logging.go
178178179179+// RequireHTMXMiddleware ensures that certain API routes are only accessible via HTMX requests.
180180+// This prevents direct browser access to internal API endpoints that return fragments or JSON.
181181+// Routes that need to be publicly accessible (like /api/resolve-handle) should not use this middleware.
182182+func RequireHTMXMiddleware(next http.Handler) http.Handler {
183183+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
184184+ // Check for HTMX request header
185185+ if r.Header.Get("HX-Request") != "true" {
186186+ http.NotFound(w, r)
187187+ return
188188+ }
189189+ next.ServeHTTP(w, r)
190190+ })
191191+}
192192+179193// MaxBodySize limits the size of request bodies
180194const (
181195 MaxJSONBodySize = 1 << 20 // 1 MB for JSON requests
+9-12
internal/routing/routing.go
···3434 mux.HandleFunc("GET /.well-known/oauth-client-metadata", h.HandleWellKnownOAuth)
35353636 // API routes for handle resolution (used by login autocomplete)
3737+ // These are intentionally public and don't require HTMX headers
3738 mux.HandleFunc("GET /api/resolve-handle", h.HandleResolveHandle)
3839 mux.HandleFunc("GET /api/search-actors", h.HandleSearchActors)
39404040- // API route for fetching all user data (used by client-side cache)
4141+ // API route for fetching all user data (used by client-side cache via fetch())
4242+ // Auth-protected but accessible without HTMX header (called from JavaScript)
4143 mux.HandleFunc("GET /api/data", h.HandleAPIListAll)
42444343- // Community feed partial (loaded async via HTMX)
4444- mux.HandleFunc("GET /api/feed", h.HandleFeedPartial)
4545-4646- // Brew list partial (loaded async via HTMX)
4747- mux.HandleFunc("GET /api/brews", h.HandleBrewListPartial)
4848-4949- // Manage page partial (loaded async via HTMX)
5050- mux.HandleFunc("GET /api/manage", h.HandleManagePartial)
5151-5252- // Profile content partial (loaded async via HTMX)
5353- mux.HandleFunc("GET /api/profile/{actor}", h.HandleProfilePartial)
4545+ // HTMX partials (loaded async via HTMX)
4646+ // These return HTML fragments and should only be accessed via HTMX
4747+ mux.Handle("GET /api/feed", middleware.RequireHTMXMiddleware(http.HandlerFunc(h.HandleFeedPartial)))
4848+ mux.Handle("GET /api/brews", middleware.RequireHTMXMiddleware(http.HandlerFunc(h.HandleBrewListPartial)))
4949+ mux.Handle("GET /api/manage", middleware.RequireHTMXMiddleware(http.HandlerFunc(h.HandleManagePartial)))
5050+ mux.Handle("GET /api/profile/{actor}", middleware.RequireHTMXMiddleware(http.HandlerFunc(h.HandleProfilePartial)))
54515552 // Page routes (must come before static files)
5653 mux.HandleFunc("GET /{$}", h.HandleHome) // {$} means exact match