···1111- **Testing:** Standard library testing + [shutter](https://github.com/ptdewey/shutter) for snapshot tests
1212- **Logging:** zerolog
13131414+## Use Go Tooling Effectively
1515+1616+- To see source files from a dependency, or to answer questions
1717+ about a dependency, run `go mod download -json MODULE` and use
1818+ the returned `Dir` path to read the files.
1919+2020+- Use `go doc foo.Bar` or `go doc -all foo` to read documentation
2121+ for packages, types, functions, etc.
2222+2323+- Use `go run .` or `go run ./cmd/foo` instead of `go build` to
2424+ run programs, to avoid leaving behind build artifacts.
2525+1426## Project Structure
15271628```
···143155**Location:** `internal/handlers/api_snapshot_test.go`
144156145157**Covered endpoints:**
158158+146159- Authentication: `/api/me`, `/client-metadata.json`
147160- Data fetching: `/api/data`, `/api/feed-json`, `/api/profile-json/{actor}`
148161- CRUD operations: Create/Update/Delete for beans, roasters, grinders, brewers, brews
149162150163**Running snapshot tests:**
164164+151165```bash
152166cd internal/handlers && go test -v -run "Snapshot"
153167```
154168155169**Working with snapshots:**
170170+156171```bash
157172# Accept all new/changed snapshots
158173shutter accept-all
···165180```
166181167182**Snapshot patterns used:**
183183+168184- `shutter.ScrubTimestamp()` - Removes timestamp values for deterministic tests
169185- `shutter.IgnoreKey("created_at")` - Ignores specific JSON keys
170186- `shutter.IgnoreKey("rkey")` - Ignores AT Protocol record keys (TIDs are time-based)
···177193go build -o arabica cmd/arabica-server/main.go
178194```
179195180180-## Command-Line Flags
181181-182182-| Flag | Type | Default | Description |
183183-| --------------- | ------ | ------- | ----------------------------------------------------- |
184184-| `--firehose` | bool | true | [DEPRECATED] Firehose is now the default (ignored) |
185185-| `--known-dids` | string | "" | Path to file with DIDs to backfill (one per line) |
186186-187187-**Known DIDs File Format:**
188188-- One DID per line (e.g., `did:plc:abc123xyz`)
189189-- Lines starting with `#` are comments
190190-- Empty lines are ignored
191191-- See `known-dids.txt.example` for reference
192192-193196## Environment Variables
194197195195-| Variable | Default | Description |
196196-| --------------------------- | --------------------------------- | ---------------------------------- |
197197-| `PORT` | 18910 | HTTP server port |
198198-| `SERVER_PUBLIC_URL` | - | Public URL for reverse proxy (enables secure cookies when HTTPS) |
199199-| `ARABICA_DB_PATH` | ~/.local/share/arabica/arabica.db | BoltDB path (sessions, registry) |
200200-| `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path |
201201-| `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration |
202202-| `LOG_LEVEL` | info | debug/info/warn/error |
203203-| `LOG_FORMAT` | console | console/json |
198198+| Variable | Default | Description |
199199+| --------------------------- | ------------------------------------ | ---------------------------------------------------------------- |
200200+| `PORT` | 18910 | HTTP server port |
201201+| `SERVER_PUBLIC_URL` | - | Public URL for reverse proxy (enables secure cookies when HTTPS) |
202202+| `ARABICA_DB_PATH` | ~/.local/share/arabica/arabica.db | BoltDB path (sessions, registry) |
203203+| `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path |
204204+| `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration |
205205+| `LOG_LEVEL` | info | debug/info/warn/error |
206206+| `LOG_FORMAT` | console | console/json |
204207205208## Code Patterns
206209···353356354357## Known Issues / TODOs
355358356356-Key areas:
357357-358358-- Context should flow through methods (some fixed, verify all paths)
359359-- Cache race conditions need copy-on-write pattern
360360-- Missing CID validation on record updates (AT Protocol best practice)
361361-- Rate limiting for PDS calls not implemented
359359+See @BACKLOG.md
+16-2
BACKLOG.md
···2020 - Manage + brews list together probably makes sense
21212222- IMPORTANT: If this platform gains any traction, we will need some form of content moderation
2323- - Due to the nature of arabica, this will only really be text based (text and hyperlinks)
2323+ - Due to the nature of arabica, this will only really need to be text based (text and hyperlinks)
2424 - Malicious link scanning may be reasonable, not sure about deeper text analysis
2525 - Need to do more research into security
2626 - Need admin tooling at the app level that will allow deleting records (may not be possible),
2727 removing from appview, blacklisting users (and maybe IPs?), possibly more
2828 - Having accounts with admin rights may be an approach to this (configured with flags at startup time?)
2929 @arabica.social, @pdewey.com, maybe others? (need trusted users in other time zones probably)
3030+ - Add on some piece to the TOS that mentions I reserve the right to de-list content from the platform
3131+ - Continue limiting firehose posts to users who have been previously authenticated (keep a permanent record of "trusted" users)
3232+ - By logging in users agree to TOS -- can create records to be displayed on the appview ("signal" records)
3333+ Attestation signature from appview (or pds -- use key from pds) was source of record being created
3434+ - This is a pretty important consideration going forward, lots to consider
30353136## Features
3237···5762 - Might be able to just save to the db when backfilling a profile's records
5863 - NOTE: requires research into existing solustions (whatever tangled does is probably good)
59646565+- Opengraph metadata in brew entry page, to allow rich embeds in bluesky
6666+ - All pages should have opengraph metadat, but view brew, profile, and home/feed are probably the most important
6767+6868+- Maybe move water amount below pours in form, sum pours if they are entered first.
6969+ - Would need to not override if water amount is entered after pours
7070+ (maybe update after leaving pour input?).
7171+6072## Fixes
61736274- Migrate terms page text. Add links to about at top of non-authed home page
···69817082- Show "view" button on brews in profile page (same as on brews list page)
71837272-- Fix nix build, nix run, to build frontend as well
8484+- The "back" button behaves kind of strangely
8585+ - Goes back to brews list after clicking on view bean in feed,
8686+ takes to profile for other users' brews.
-326
CLAUDE.md
···11-# Arabica - Project Context for AI Agents
22-33-Coffee brew tracking application using AT Protocol for decentralized storage.
44-55-## Tech Stack
66-77-- **Language:** Go 1.21+
88-- **HTTP:** stdlib `net/http` with Go 1.22 routing
99-- **Storage:** AT Protocol PDS (user data), BoltDB (sessions/feed registry)
1010-- **Frontend:** Svelte SPA with client-side routing
1111-- **Legacy:** HTMX partials still used for some dynamic content (being phased out)
1212-- **Logging:** zerolog
1313-1414-## Project Structure
1515-1616-```
1717-cmd/arabica-server/main.go # Application entry point
1818-internal/
1919- atproto/ # AT Protocol integration
2020- client.go # Authenticated PDS client (XRPC calls)
2121- oauth.go # OAuth flow with PKCE/DPOP
2222- store.go # database.Store implementation using PDS
2323- cache.go # Per-session in-memory cache
2424- records.go # Model <-> ATProto record conversion
2525- resolver.go # AT-URI parsing and reference resolution
2626- public_client.go # Unauthenticated public API access
2727- nsid.go # Collection NSIDs and AT-URI builders
2828- handlers/
2929- handlers.go # HTTP handlers (API endpoints + HTMX partials)
3030- auth.go # OAuth login/logout/callback
3131- bff/
3232- render.go # Legacy template rendering (HTMX partials only)
3333- helpers.go # View helpers (formatting, etc.)
3434- database/
3535- store.go # Store interface definition
3636- boltstore/ # BoltDB implementation for sessions
3737- feed/
3838- service.go # Community feed aggregation
3939- registry.go # User registration for feed
4040- models/
4141- models.go # Domain models and request types
4242- middleware/
4343- logging.go # Request logging middleware
4444- routing/
4545- routing.go # Router setup and middleware chain
4646-frontend/ # Svelte SPA source code
4747- src/
4848- routes/ # Page components
4949- components/ # Reusable components
5050- stores/ # Svelte stores (auth, cache)
5151- lib/ # Utilities (router, API client)
5252- public/ # Built SPA assets
5353-lexicons/ # AT Protocol lexicon definitions (JSON)
5454-templates/partials/ # Legacy HTMX partial templates (being phased out)
5555-static/ # Static assets (CSS, icons, service worker)
5656- app/ # Built Svelte SPA
5757-```
5858-5959-## Key Concepts
6060-6161-### AT Protocol Integration
6262-6363-User data stored in their Personal Data Server (PDS), not locally. The app:
6464-6565-1. Authenticates via OAuth (indigo SDK handles PKCE/DPOP)
6666-2. Gets access token scoped to user's DID
6767-3. Performs CRUD via XRPC calls to user's PDS
6868-6969-**Collections (NSIDs):**
7070-7171-- `social.arabica.alpha.bean` - Coffee beans
7272-- `social.arabica.alpha.roaster` - Roasters
7373-- `social.arabica.alpha.grinder` - Grinders
7474-- `social.arabica.alpha.brewer` - Brewing devices
7575-- `social.arabica.alpha.brew` - Brew sessions (references bean, grinder, brewer)
7676-7777-**Record keys:** TID format (timestamp-based identifiers)
7878-7979-**References:** Records reference each other via AT-URIs (`at://did/collection/rkey`)
8080-8181-### Store Interface
8282-8383-`internal/database/store.go` defines the `Store` interface. Two implementations:
8484-8585-- `AtprotoStore` - Production, stores in user's PDS
8686-- BoltDB stores only sessions and feed registry (not user data)
8787-8888-All Store methods take `context.Context` as first parameter.
8989-9090-### Request Flow
9191-9292-1. Request hits middleware (logging, auth check)
9393-2. Auth middleware extracts DID + session ID from cookies
9494-3. For SPA routes: Serve index.html (client-side routing)
9595-4. For API routes: Handler creates `AtprotoStore` scoped to user
9696-5. Store methods make XRPC calls to user's PDS
9797-6. Results returned as JSON (for SPA) or HTML fragments (legacy HTMX partials)
9898-9999-### Caching
100100-101101-`SessionCache` caches user data in memory (5-minute TTL):
102102-103103-- Avoids repeated PDS calls for same data
104104-- Invalidated on writes
105105-- Background cleanup removes expired entries
106106-107107-### Backfill Strategy
108108-109109-User records are backfilled from their PDS once per DID:
110110-111111-- **On startup**: Backfills registered users + known-dids file
112112-- **On first login**: Backfills the user's historical records
113113-- **Deduplication**: Tracks backfilled DIDs in `BucketBackfilled` to prevent redundant fetches
114114-- **Idempotent**: Safe to call multiple times (checks backfill status first)
115115-116116-This prevents excessive PDS requests while ensuring new users' historical data is indexed.
117117-118118-## Common Tasks
119119-120120-### Run Development Server
121121-122122-```bash
123123-# Run server (uses firehose mode by default)
124124-go run cmd/arabica-server/main.go
125125-126126-# Backfill known DIDs on startup
127127-go run cmd/arabica-server/main.go --known-dids known-dids.txt
128128-129129-# Using nix
130130-nix run
131131-```
132132-133133-### Run Tests
134134-135135-```bash
136136-go test ./...
137137-```
138138-139139-### Build
140140-141141-```bash
142142-go build -o arabica cmd/arabica-server/main.go
143143-```
144144-145145-## Command-Line Flags
146146-147147-| Flag | Type | Default | Description |
148148-| --------------- | ------ | ------- | ----------------------------------------------------- |
149149-| `--firehose` | bool | true | [DEPRECATED] Firehose is now the default (ignored) |
150150-| `--known-dids` | string | "" | Path to file with DIDs to backfill (one per line) |
151151-152152-**Known DIDs File Format:**
153153-- One DID per line (e.g., `did:plc:abc123xyz`)
154154-- Lines starting with `#` are comments
155155-- Empty lines are ignored
156156-- See `known-dids.txt.example` for reference
157157-158158-## Environment Variables
159159-160160-| Variable | Default | Description |
161161-| --------------------------- | --------------------------------- | ---------------------------------- |
162162-| `PORT` | 18910 | HTTP server port |
163163-| `SERVER_PUBLIC_URL` | - | Public URL for reverse proxy (enables secure cookies when HTTPS) |
164164-| `ARABICA_DB_PATH` | ~/.local/share/arabica/arabica.db | BoltDB path (sessions, registry) |
165165-| `ARABICA_FEED_INDEX_PATH` | ~/.local/share/arabica/feed-index.db | Firehose index BoltDB path |
166166-| `ARABICA_PROFILE_CACHE_TTL` | 1h | Profile cache duration |
167167-| `LOG_LEVEL` | info | debug/info/warn/error |
168168-| `LOG_FORMAT` | console | console/json |
169169-170170-## Code Patterns
171171-172172-### Creating a Store
173173-174174-```go
175175-// In handlers, store is created per-request
176176-store, authenticated := h.getAtprotoStore(r)
177177-if !authenticated {
178178- http.Error(w, "Authentication required", http.StatusUnauthorized)
179179- return
180180-}
181181-182182-// Use store with request context
183183-brews, err := store.ListBrews(r.Context(), userID)
184184-```
185185-186186-### Record Conversion
187187-188188-```go
189189-// Model -> ATProto record
190190-record, err := BrewToRecord(brew, beanURI, grinderURI, brewerURI)
191191-192192-// ATProto record -> Model
193193-brew, err := RecordToBrew(record, atURI)
194194-```
195195-196196-### AT-URI Handling
197197-198198-```go
199199-// Build AT-URI
200200-uri := BuildATURI(did, NSIDBean, rkey) // at://did:plc:xxx/social.arabica.alpha.bean/abc
201201-202202-// Parse AT-URI
203203-components, err := ResolveATURI(uri)
204204-// components.DID, components.Collection, components.RKey
205205-```
206206-207207-## Future Vision: Social Features
208208-209209-The app currently has a basic community feed. Future plans expand social interactions leveraging AT Protocol's decentralized nature.
210210-211211-### Planned Lexicons
212212-213213-```
214214-social.arabica.alpha.like - Like a brew (references brew AT-URI)
215215-social.arabica.alpha.comment - Comment on a brew
216216-social.arabica.alpha.follow - Follow another user
217217-social.arabica.alpha.share - Re-share a brew to your feed
218218-```
219219-220220-### Like Record (Planned)
221221-222222-```json
223223-{
224224- "lexicon": 1,
225225- "id": "social.arabica.alpha.like",
226226- "defs": {
227227- "main": {
228228- "type": "record",
229229- "key": "tid",
230230- "record": {
231231- "type": "object",
232232- "required": ["subject", "createdAt"],
233233- "properties": {
234234- "subject": {
235235- "type": "ref",
236236- "ref": "com.atproto.repo.strongRef",
237237- "description": "The brew being liked"
238238- },
239239- "createdAt": { "type": "string", "format": "datetime" }
240240- }
241241- }
242242- }
243243- }
244244-}
245245-```
246246-247247-### Comment Record (Planned)
248248-249249-```json
250250-{
251251- "lexicon": 1,
252252- "id": "social.arabica.alpha.comment",
253253- "defs": {
254254- "main": {
255255- "type": "record",
256256- "key": "tid",
257257- "record": {
258258- "type": "object",
259259- "required": ["subject", "text", "createdAt"],
260260- "properties": {
261261- "subject": {
262262- "type": "ref",
263263- "ref": "com.atproto.repo.strongRef",
264264- "description": "The brew being commented on"
265265- },
266266- "text": {
267267- "type": "string",
268268- "maxLength": 1000,
269269- "maxGraphemes": 300
270270- },
271271- "createdAt": { "type": "string", "format": "datetime" }
272272- }
273273- }
274274- }
275275- }
276276-}
277277-```
278278-279279-### Implementation Approach
280280-281281-**Cross-user interactions:**
282282-283283-- Likes/comments stored in the actor's PDS (not the brew owner's)
284284-- Use `public_client.go` to read other users' brews
285285-- Aggregate likes/comments via relay/firehose or direct PDS queries
286286-287287-**Feed aggregation:**
288288-289289-- Current: Poll registered users' PDS for brews
290290-- Future: Subscribe to firehose for real-time updates
291291-- Index social interactions in local DB for fast queries
292292-293293-**UI patterns:**
294294-295295-- Like button on brew cards in feed
296296-- Comment thread below brew detail view
297297-- Share button to re-post with optional note
298298-- Notification system for interactions on your brews
299299-300300-### Key Design Decisions
301301-302302-1. **Strong references** - Likes/comments use `com.atproto.repo.strongRef` (URI + CID) to ensure the referenced brew hasn't changed
303303-2. **Actor-owned data** - Your likes live in your PDS, not the brew owner's
304304-3. **Public by default** - Social interactions are public records, readable by anyone
305305-4. **Portable identity** - Users can switch PDS and keep their social graph
306306-307307-## Deployment Notes
308308-309309-### CSS Cache Busting
310310-311311-When making CSS/style changes, bump the version query parameter in `templates/layout.tmpl`:
312312-313313-```html
314314-<link rel="stylesheet" href="/static/css/output.css?v=0.1.3" />
315315-```
316316-317317-Cloudflare caches static assets, so incrementing the version ensures users get the updated styles.
318318-319319-## Known Issues / TODOs
320320-321321-Key areas:
322322-323323-- Context should flow through methods (some fixed, verify all paths)
324324-- Cache race conditions need copy-on-write pattern
325325-- Missing CID validation on record updates (AT Protocol best practice)
326326-- Rate limiting for PDS calls not implemented
+1-22
README.md
···2233Coffee brew tracking application build on ATProto
4455-Development is on GitHub, and is mirrored to Tangled:
55+Development is primarily happening on Tangled, and is mirrored to GitHub:
6677- [Tangled](https://tangled.org/arabica.social/arabica)
88- [GitHub](https://github.com/arabica-social/arabica)
991010-GitHub is currently the primary repo, but that may change in the future.
1111-1210## Features
13111412- Track coffee brews with detailed parameters
1513- Store data in your AT Protocol Personal Data Server
1614- Community feed of recent brews from registered users (polling or real-time firehose)
1715- Manage beans, roasters, grinders, and brewers
1818-- Export brew data as JSON
1916- Mobile-friendly PWA design
2020-2121-## Tech Stack
2222-2323-- Backend: Go with stdlib HTTP router
2424-- Storage: AT Protocol Personal Data Servers + BoltDB for local cache
2525-- Templates: html/template
2626-- Frontend: HTMX + Alpine.js + Tailwind CSS
27172818## Quick Start
2919···6959- `OAUTH_REDIRECT_URI` - OAuth redirect URI (optional)
7060- `LOG_LEVEL` - Logging level: debug, info, warn, error (default: info)
7161- `LOG_FORMAT` - Log format: console, json (default: console)
7272-7373-## Architecture
7474-7575-Data is stored in AT Protocol records on users' Personal Data Servers. The application uses OAuth to authenticate with the PDS and performs all CRUD operations via the AT Protocol API.
7676-7777-Local BoltDB stores:
7878-7979-- OAuth session data
8080-- Feed registry (list of DIDs for community feed)
8181-8282-See docs/ for detailed documentation.
83628463## Development
8564
+1050
docs/likes-follows-comments-plan.md
···11+# Arabica Social Features Plan: Likes, Follows, and Comments
22+33+**Version:** 1.0
44+**Date:** January 25, 2026
55+**Status:** Planning
66+77+---
88+99+TODO:
1010+1111+- This is not going to be the current state, I don't love the plan claude made
1212+- Likes will probably be their own lexicon (maybe with a lens to bsky likes? -- probably not)
1313+- Comments tbd, I would like to avoid forcing users onto bsky for social features though
1414+- Follows, allow importing social graph from bsky (might be able to use a sort of statndardized lexicon here?)
1515+ - Likely creating a custom lexicon that is structurally similar/the same as bsky (maybe standard.site sub/pub lex if that would work?)
1616+1717+---
1818+1919+## Executive Summary
2020+2121+This document outlines the implementation plan for adding social features to Arabica: likes, follows, and comments. The plan leverages AT Protocol's decentralized architecture while evaluating strategic reuse of Bluesky's existing social lexicons versus creating Arabica-specific ones.
2222+2323+## Table of Contents
2424+2525+1. [Goals & Non-Goals](#goals--non-goals)
2626+2. [Architecture Overview](#architecture-overview)
2727+3. [Lexicon Design Decisions](#lexicon-design-decisions)
2828+4. [Implementation Phases](#implementation-phases)
2929+5. [Technical Details](#technical-details)
3030+6. [Bluesky Integration Strategies](#bluesky-integration-strategies)
3131+7. [Data Flow & Storage](#data-flow--storage)
3232+8. [UI/UX Considerations](#uiux-considerations)
3333+9. [Migration & Rollout](#migration--rollout)
3434+10. [Future Enhancements](#future-enhancements)
3535+3636+---
3737+3838+## Goals & Non-Goals
3939+4040+### Goals
4141+4242+- **Enable likes** on brews, beans, roasters, grinders, and brewers
4343+- **Support follows** to create personalized feeds of coffee enthusiasts
4444+- **Add comments** to enable discussions around brews and equipment
4545+- **Maintain decentralization**: Social interactions stored in users' own PDS
4646+- **Leverage existing infrastructure**: Use Bluesky's lexicons where beneficial
4747+- **Preserve portability**: Users can take their data anywhere
4848+- **Enable discoverability**: Surface popular content and active users
4949+5050+### Non-Goals
5151+5252+- Building a full social network (messaging, DMs, notifications beyond basic)
5353+- Implementing moderation tools (initial phase)
5454+- Creating a mobile app (web-first approach)
5555+- Supporting multimedia beyond existing image support
5656+5757+---
5858+5959+## Architecture Overview
6060+6161+### Current State
6262+6363+```
6464+User's PDS
6565+├── social.arabica.alpha.bean (coffee beans)
6666+├── social.arabica.alpha.roaster (roasters)
6767+├── social.arabica.alpha.grinder (grinders)
6868+├── social.arabica.alpha.brewer (brewing devices)
6969+└── social.arabica.alpha.brew (brew sessions)
7070+7171+Arabica Server
7272+├── Firehose Listener (crawls network for brew data)
7373+├── Feed Index (BoltDB - aggregated feed)
7474+├── Session Store (BoltDB - sessions/registry)
7575+└── Profile Cache (in-memory, 1hr TTL)
7676+```
7777+7878+### Proposed State
7979+8080+```
8181+User's PDS
8282+├── Arabica Records
8383+│ ├── social.arabica.alpha.bean
8484+│ ├── social.arabica.alpha.roaster
8585+│ ├── social.arabica.alpha.grinder
8686+│ ├── social.arabica.alpha.brewer
8787+│ └── social.arabica.alpha.brew
8888+│
8989+├── Social Interactions (Option A: Arabica-specific)
9090+│ ├── social.arabica.alpha.like
9191+│ ├── social.arabica.alpha.follow
9292+│ └── social.arabica.alpha.comment
9393+│
9494+└── Social Interactions (Option B: Bluesky lexicons)
9595+ ├── app.bsky.feed.like (reuse for likes)
9696+ ├── app.bsky.graph.follow (reuse for follows)
9797+ └── social.arabica.alpha.comment (custom for comments)
9898+9999+Arabica Server
100100+├── Firehose Listener (+ like/follow/comment indexing)
101101+├── Social Index (BoltDB - likes, follows, comments)
102102+├── Feed Index (enhanced with social signals)
103103+├── Session Store
104104+└── Profile Cache
105105+```
106106+107107+---
108108+109109+## Lexicon Design Decisions
110110+111111+### Decision Matrix
112112+113113+| Feature | Custom Lexicon | Bluesky Lexicon | Recommendation |
114114+| ------------ | ------------------------------ | ------------------------------ | ----------------- |
115115+| **Likes** | `social.arabica.alpha.like` | `app.bsky.feed.like` | **Use Bluesky** |
116116+| **Follows** | `social.arabica.alpha.follow` | `app.bsky.graph.follow` | **Use Bluesky** |
117117+| **Comments** | `social.arabica.alpha.comment` | `app.bsky.feed.post` (replies) | **Create Custom** |
118118+119119+### Rationale
120120+121121+#### ✅ Use `app.bsky.feed.like` for Likes
122122+123123+**Pros:**
124124+125125+- Simple, well-tested schema (just subject + timestamp)
126126+- Enables cross-app discoverability (Bluesky users can see popular coffee content)
127127+- No need to maintain our own lexicon
128128+- Future compatibility with Bluesky social graph features
129129+- Users' existing Bluesky likes are already in their PDS
130130+131131+**Cons:**
132132+133133+- Couples us to Bluesky's schema evolution
134134+- Mixing Arabica and Bluesky content in like feeds
135135+136136+**Schema:**
137137+138138+```json
139139+{
140140+ "lexicon": 1,
141141+ "id": "app.bsky.feed.like",
142142+ "defs": {
143143+ "main": {
144144+ "type": "record",
145145+ "key": "tid",
146146+ "record": {
147147+ "type": "object",
148148+ "required": ["subject", "createdAt"],
149149+ "properties": {
150150+ "subject": {
151151+ "type": "ref",
152152+ "ref": "com.atproto.repo.strongRef",
153153+ "description": "AT-URI + CID of the liked record"
154154+ },
155155+ "createdAt": {
156156+ "type": "string",
157157+ "format": "datetime"
158158+ }
159159+ }
160160+ }
161161+ }
162162+ }
163163+}
164164+```
165165+166166+**Example Record:**
167167+168168+```json
169169+{
170170+ "$type": "app.bsky.feed.like",
171171+ "subject": {
172172+ "uri": "at://did:plc:user123/social.arabica.alpha.brew/abc123",
173173+ "cid": "bafyreibjifzpqj6o6wcq3hejh7y4z4z2vmiklkvykc57tw3pcbx3kxifpm"
174174+ },
175175+ "createdAt": "2026-01-25T12:30:00.000Z"
176176+}
177177+```
178178+179179+#### ✅ Use `app.bsky.graph.follow` for Follows
180180+181181+**Pros:**
182182+183183+- Standard social graph representation
184184+- Interoperability: Arabica follows visible in Bluesky social graph
185185+- Enables "import follows from Bluesky" (see below)
186186+- Could power recommendations ("Users who brew X also follow Y")
187187+- Simplifies social graph queries
188188+189189+**Cons:**
190190+191191+- Follow graph will mix Arabica and Bluesky users
192192+- Need to filter by context in queries
193193+194194+**Schema:**
195195+196196+```json
197197+{
198198+ "lexicon": 1,
199199+ "id": "app.bsky.graph.follow",
200200+ "defs": {
201201+ "main": {
202202+ "type": "record",
203203+ "key": "tid",
204204+ "record": {
205205+ "type": "object",
206206+ "required": ["subject", "createdAt"],
207207+ "properties": {
208208+ "subject": {
209209+ "type": "string",
210210+ "format": "did",
211211+ "description": "DID of the user being followed"
212212+ },
213213+ "createdAt": {
214214+ "type": "string",
215215+ "format": "datetime"
216216+ }
217217+ }
218218+ }
219219+ }
220220+ }
221221+}
222222+```
223223+224224+**Example Record:**
225225+226226+```json
227227+{
228228+ "$type": "app.bsky.graph.follow",
229229+ "subject": "did:plc:coffee-enthusiast-456",
230230+ "createdAt": "2026-01-25T12:30:00.000Z"
231231+}
232232+```
233233+234234+#### ✅ Create `social.arabica.alpha.comment` for Comments
235235+236236+**Pros:**
237237+238238+- Coffee-specific comment features (e.g., ratings, tasting notes)
239239+- Can extend with Arabica-specific fields
240240+- Cleaner separation from Bluesky post threads
241241+- No confusion between "replies" and "comments"
242242+243243+**Cons:**
244244+245245+- Maintains another lexicon
246246+- Comments won't appear in Bluesky's thread views
247247+- Need to build our own comment threading
248248+249249+**Proposed Schema:**
250250+251251+```json
252252+{
253253+ "lexicon": 1,
254254+ "id": "social.arabica.alpha.comment",
255255+ "defs": {
256256+ "main": {
257257+ "type": "record",
258258+ "key": "tid",
259259+ "description": "A comment on a brew or equipment",
260260+ "record": {
261261+ "type": "object",
262262+ "required": ["subject", "text", "createdAt"],
263263+ "properties": {
264264+ "subject": {
265265+ "type": "ref",
266266+ "ref": "com.atproto.repo.strongRef",
267267+ "description": "The brew/bean/roaster/etc being commented on"
268268+ },
269269+ "text": {
270270+ "type": "string",
271271+ "maxLength": 2000,
272272+ "maxGraphemes": 500,
273273+ "description": "Comment text"
274274+ },
275275+ "parent": {
276276+ "type": "ref",
277277+ "ref": "com.atproto.repo.strongRef",
278278+ "description": "Parent comment for threading (optional)"
279279+ },
280280+ "facets": {
281281+ "type": "array",
282282+ "description": "Mentions, links, hashtags",
283283+ "items": {
284284+ "type": "ref",
285285+ "ref": "app.bsky.richtext.facet"
286286+ }
287287+ },
288288+ "rating": {
289289+ "type": "integer",
290290+ "minimum": 1,
291291+ "maximum": 10,
292292+ "description": "Optional rating (1-10)"
293293+ },
294294+ "createdAt": {
295295+ "type": "string",
296296+ "format": "datetime"
297297+ }
298298+ }
299299+ }
300300+ }
301301+ }
302302+}
303303+```
304304+305305+**Example Record:**
306306+307307+```json
308308+{
309309+ "$type": "social.arabica.alpha.comment",
310310+ "subject": {
311311+ "uri": "at://did:plc:user123/social.arabica.alpha.brew/xyz789",
312312+ "cid": "bafyreig2fjxi3rptqdgylg7e5hmjl6mcke7rn2b6cugzlqq3i4zu6rq52q"
313313+ },
314314+ "text": "Lovely floral notes! What was your water temp?",
315315+ "rating": 8,
316316+ "createdAt": "2026-01-25T14:00:00.000Z"
317317+}
318318+```
319319+320320+---
321321+322322+## Implementation Phases
323323+324324+### Phase 1: Likes (2-3 weeks)
325325+326326+**Deliverables:**
327327+328328+1. ✅ Lexicon decision: Use `app.bsky.feed.like`
329329+2. Backend: Index likes from firehose
330330+3. Backend: Aggregate like counts per record
331331+4. Backend: API endpoints for liking/unliking
332332+5. Frontend: Like button UI on brew cards
333333+6. Frontend: Display like counts
334334+7. Testing: Snapshot tests for like endpoints
335335+336336+**Technical Tasks:**
337337+338338+- Update firehose listener to capture `app.bsky.feed.like` records
339339+- Add `LikesIndex` to BoltDB (keyed by subject AT-URI)
340340+- Implement `GetLikeCount(uri string)` function
341341+- Implement `UserHasLiked(userDID, uri string)` function
342342+- Create/delete like via PDS client
343343+- Frontend: Like button component with optimistic updates
344344+345345+**Database Schema (BoltDB):**
346346+347347+```
348348+Bucket: Likes
349349+Key: <subject-at-uri>
350350+Value: {
351351+ "count": 42,
352352+ "recent": ["did:plc:user1", "did:plc:user2", ...] // last 10 likers
353353+}
354354+355355+Bucket: UserLikes
356356+Key: <user-did>/<subject-at-uri>
357357+Value: <like-record-uri> // for quick "has user liked this?" checks
358358+```
359359+360360+### Phase 2: Follows (3-4 weeks)
361361+362362+**Deliverables:**
363363+364364+1. ✅ Lexicon decision: Use `app.bsky.graph.follow`
365365+2. Backend: Index follows from firehose
366366+3. Backend: Build follower/following graph
367367+4. Backend: Personalized feed based on follows
368368+5. Frontend: Follow button on user profiles
369369+6. Frontend: Followers/Following pages
370370+7. Feature: Import follows from Bluesky (see below)
371371+372372+**Technical Tasks:**
373373+374374+- Update firehose listener to capture `app.bsky.graph.follow` records
375375+- Add `FollowsIndex` to BoltDB
376376+- Implement `GetFollowers(did string)` function
377377+- Implement `GetFollowing(did string)` function
378378+- Implement `UserFollows(followerDID, followedDID string)` function
379379+- Create/delete follow via PDS client
380380+- Frontend: Follow button component
381381+- Frontend: "Following" feed filter
382382+383383+**Database Schema (BoltDB):**
384384+385385+```
386386+Bucket: Follows
387387+Key: follower:<did>
388388+Value: ["did:plc:followed1", "did:plc:followed2", ...]
389389+390390+Bucket: Followers
391391+Key: followed:<did>
392392+Value: ["did:plc:follower1", "did:plc:follower2", ...]
393393+394394+Bucket: FollowCounts
395395+Key: <did>
396396+Value: {
397397+ "followers": 120,
398398+ "following": 87
399399+}
400400+```
401401+402402+### Phase 3: Comments (4-5 weeks)
403403+404404+**Deliverables:**
405405+406406+1. ✅ Lexicon: Create `social.arabica.alpha.comment`
407407+2. Backend: Index comments from firehose
408408+3. Backend: Comment threading logic
409409+4. Backend: Comment counts per record
410410+5. Frontend: Comment display UI
411411+6. Frontend: Comment creation form
412412+7. Frontend: Comment threading/replies
413413+414414+**Technical Tasks:**
415415+416416+- Define and publish `social.arabica.alpha.comment` lexicon
417417+- Update firehose listener to capture comment records
418418+- Add `CommentsIndex` to BoltDB
419419+- Implement `GetComments(uri string, limit, offset int)` function
420420+- Implement comment threading/tree building
421421+- Create comment via PDS client
422422+- Frontend: Comment list component
423423+- Frontend: Comment form with mentions/facets support
424424+425425+**Database Schema (BoltDB):**
426426+427427+```
428428+Bucket: Comments
429429+Key: <subject-at-uri>/<timestamp>
430430+Value: {
431431+ "author": "did:plc:user1",
432432+ "text": "Great brew!",
433433+ "parent": "at://...", // null for top-level
434434+ "rating": 9,
435435+ "createdAt": "2026-01-25T12:00:00Z",
436436+ "uri": "at://did:plc:user1/social.arabica.alpha.comment/abc123"
437437+}
438438+439439+Bucket: CommentCounts
440440+Key: <subject-at-uri>
441441+Value: 15
442442+```
443443+444444+### Phase 4: Social Feed Enhancements (2-3 weeks)
445445+446446+**Deliverables:**
447447+448448+1. Following-only feed view
449449+2. Popular brews (by like count)
450450+3. Trending equipment
451451+4. Active users widget
452452+5. Social notifications (basic)
453453+454454+---
455455+456456+## Technical Details
457457+458458+### 1. Firehose Integration
459459+460460+**Current:**
461461+462462+- Listens for `social.arabica.alpha.*` records
463463+- Indexes brews, beans, roasters, grinders, brewers
464464+465465+**Enhanced:**
466466+467467+```go
468468+// internal/firehose/listener.go
469469+470470+func (l *Listener) handleFirehoseEvent(evt *events.RepoCommit) {
471471+ for _, op := range evt.Ops {
472472+ switch op.Collection {
473473+ // Existing collections
474474+ case atproto.NSIDBrew, atproto.NSIDBean, atproto.NSIDRoaster,
475475+ atproto.NSIDGrinder, atproto.NSIDBrewer:
476476+ l.handleArabicaRecord(op)
477477+478478+ // Social interactions
479479+ case "app.bsky.feed.like":
480480+ l.handleLike(op)
481481+ case "app.bsky.graph.follow":
482482+ l.handleFollow(op)
483483+ case atproto.NSIDComment: // social.arabica.alpha.comment
484484+ l.handleComment(op)
485485+ }
486486+ }
487487+}
488488+489489+func (l *Listener) handleLike(op *events.RepoOp) error {
490490+ // Parse like record
491491+ var like atproto.Like
492492+ if err := json.Unmarshal(op.Record, &like); err != nil {
493493+ return err
494494+ }
495495+496496+ // Filter: only index likes on Arabica content
497497+ if !strings.HasPrefix(like.Subject.URI, "at://") {
498498+ return nil
499499+ }
500500+ components := atproto.ParseATURI(like.Subject.URI)
501501+ if !strings.HasPrefix(components.Collection, "social.arabica.alpha.") {
502502+ return nil // Skip non-Arabica likes
503503+ }
504504+505505+ // Index the like
506506+ return l.socialIndex.IndexLike(op.Author, &like)
507507+}
508508+```
509509+510510+### 2. API Endpoints
511511+512512+**New endpoints:**
513513+514514+```
515515+POST /api/likes # Create a like
516516+DELETE /api/likes # Unlike
517517+GET /api/likes?uri=<record-uri> # Get like count & likers
518518+519519+POST /api/follows # Follow a user
520520+DELETE /api/follows # Unfollow
521521+GET /api/followers?did=<did> # Get followers
522522+GET /api/following?did=<did> # Get following list
523523+POST /api/import-follows # Import from Bluesky
524524+525525+POST /api/comments # Create a comment
526526+GET /api/comments?uri=<uri> # Get comments for a record
527527+```
528528+529529+**Example: Like endpoint**
530530+531531+```go
532532+// internal/handlers/likes.go
533533+534534+type LikeRequest struct {
535535+ SubjectURI string `json:"uri"`
536536+ SubjectCID string `json:"cid"`
537537+}
538538+539539+func (h *Handlers) CreateLike(w http.ResponseWriter, r *http.Request) {
540540+ store, authenticated := h.getAtprotoStore(r)
541541+ if !authenticated {
542542+ http.Error(w, "Authentication required", http.StatusUnauthorized)
543543+ return
544544+ }
545545+546546+ var req LikeRequest
547547+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
548548+ http.Error(w, "Invalid request", http.StatusBadRequest)
549549+ return
550550+ }
551551+552552+ // Create like record in user's PDS
553553+ like := &atproto.Like{
554554+ Type: "app.bsky.feed.like",
555555+ Subject: &atproto.StrongRef{
556556+ URI: req.SubjectURI,
557557+ CID: req.SubjectCID,
558558+ },
559559+ CreatedAt: time.Now().Format(time.RFC3339),
560560+ }
561561+562562+ uri, err := store.CreateLike(r.Context(), like)
563563+ if err != nil {
564564+ http.Error(w, "Failed to create like", http.StatusInternalServerError)
565565+ return
566566+ }
567567+568568+ json.NewEncoder(w).Encode(map[string]string{
569569+ "uri": uri,
570570+ })
571571+}
572572+```
573573+574574+### 3. Store Interface Extensions
575575+576576+```go
577577+// internal/database/store.go
578578+579579+type Store interface {
580580+ // Existing methods...
581581+582582+ // Likes
583583+ CreateLike(ctx context.Context, like *atproto.Like) (string, error)
584584+ DeleteLike(ctx context.Context, likeURI string) error
585585+ GetLikeCount(ctx context.Context, subjectURI string) (int, error)
586586+ GetLikers(ctx context.Context, subjectURI string, limit int) ([]*Profile, error)
587587+ UserHasLiked(ctx context.Context, userDID, subjectURI string) (bool, error)
588588+589589+ // Follows
590590+ CreateFollow(ctx context.Context, follow *atproto.Follow) (string, error)
591591+ DeleteFollow(ctx context.Context, followURI string) error
592592+ GetFollowers(ctx context.Context, did string, limit, offset int) ([]*Profile, error)
593593+ GetFollowing(ctx context.Context, did string, limit, offset int) ([]*Profile, error)
594594+ UserFollows(ctx context.Context, followerDID, followedDID string) (bool, error)
595595+596596+ // Comments
597597+ CreateComment(ctx context.Context, comment *atproto.Comment) (string, error)
598598+ GetComments(ctx context.Context, subjectURI string, limit, offset int) ([]*Comment, error)
599599+ GetCommentCount(ctx context.Context, subjectURI string) (int, error)
600600+}
601601+```
602602+603603+---
604604+605605+## Bluesky Integration Strategies
606606+607607+### Import Follows from Bluesky
608608+609609+**User Story:**
610610+"As a coffee enthusiast on Bluesky, I want to import my Bluesky follows into Arabica so I can follow coffee friends without re-discovering them."
611611+612612+**Implementation:**
613613+614614+1. **Fetch Bluesky Follows**
615615+ - Use `app.bsky.graph.getFollows` API
616616+ - Query user's PDS: `GET /xrpc/app.bsky.graph.getFollows?actor={userDID}`
617617+ - Paginate through results (cursor-based)
618618+619619+2. **Filter for Arabica Users**
620620+ - Check if followed user has Arabica records
621621+ - Query: `listRecords` for `social.arabica.alpha.brew` in their PDS
622622+ - Cache results to avoid repeated lookups
623623+624624+3. **Create Follow Records**
625625+ - For each Arabica user in Bluesky follows, create `app.bsky.graph.follow` in user's PDS
626626+ - Skip if already following
627627+628628+**API Endpoint:**
629629+630630+```go
631631+POST /api/import-follows
632632+633633+Request:
634634+{
635635+ "source": "bluesky",
636636+ "filter": "arabica-users-only" // or "all"
637637+}
638638+639639+Response:
640640+{
641641+ "imported": 42,
642642+ "skipped": 8,
643643+ "failed": 1,
644644+ "details": [
645645+ {"did": "did:plc:user1", "handle": "@coffee-nerd.bsky.social", "status": "imported"},
646646+ {"did": "did:plc:user2", "handle": "@bean-expert.bsky.social", "status": "already-following"}
647647+ ]
648648+}
649649+```
650650+651651+**Implementation:**
652652+653653+```go
654654+func (h *Handlers) ImportFollows(w http.ResponseWriter, r *http.Request) {
655655+ store, authenticated := h.getAtprotoStore(r)
656656+ if !authenticated {
657657+ http.Error(w, "Authentication required", http.StatusUnauthorized)
658658+ return
659659+ }
660660+661661+ userDID := h.getUserDID(r)
662662+663663+ // 1. Fetch Bluesky follows
664664+ follows, err := h.fetchBlueskyFollows(r.Context(), userDID)
665665+ if err != nil {
666666+ http.Error(w, "Failed to fetch follows", http.StatusInternalServerError)
667667+ return
668668+ }
669669+670670+ // 2. Filter for Arabica users
671671+ arabicaUsers := []string{}
672672+ for _, follow := range follows {
673673+ hasArabicaContent, err := h.hasArabicaRecords(r.Context(), follow.DID)
674674+ if err != nil {
675675+ log.Warn().Err(err).Str("did", follow.DID).Msg("Failed to check Arabica records")
676676+ continue
677677+ }
678678+ if hasArabicaContent {
679679+ arabicaUsers = append(arabicaUsers, follow.DID)
680680+ }
681681+ }
682682+683683+ // 3. Create follow records
684684+ imported := 0
685685+ for _, targetDID := range arabicaUsers {
686686+ // Check if already following
687687+ alreadyFollows, _ := store.UserFollows(r.Context(), userDID, targetDID)
688688+ if alreadyFollows {
689689+ continue
690690+ }
691691+692692+ follow := &atproto.Follow{
693693+ Type: "app.bsky.graph.follow",
694694+ Subject: targetDID,
695695+ CreatedAt: time.Now().Format(time.RFC3339),
696696+ }
697697+ _, err := store.CreateFollow(r.Context(), follow)
698698+ if err == nil {
699699+ imported++
700700+ }
701701+ }
702702+703703+ json.NewEncoder(w).Encode(map[string]interface{}{
704704+ "imported": imported,
705705+ "total_follows": len(follows),
706706+ "arabica_users": len(arabicaUsers),
707707+ })
708708+}
709709+710710+func (h *Handlers) hasArabicaRecords(ctx context.Context, did string) (bool, error) {
711711+ // Use public client to check for any Arabica records
712712+ client := atproto.NewPublicClient()
713713+ records, err := client.ListRecords(ctx, did, atproto.NSIDBrew, 1)
714714+ if err != nil {
715715+ return false, err
716716+ }
717717+ return len(records) > 0, nil
718718+}
719719+```
720720+721721+**Challenges:**
722722+723723+- **Rate limiting**: Bluesky API has rate limits; may need to batch/queue imports
724724+- **Stale data**: Follows may be out of sync if user unfollows on Bluesky
725725+- **Performance**: Checking each DID for Arabica content is slow
726726+ - **Solution**: Maintain a "known Arabica users" index from firehose
727727+728728+**Enhancement:** Two-way sync
729729+730730+- Export Arabica follows → Bluesky follows (optional)
731731+- Periodic sync job to keep in sync
732732+733733+---
734734+735735+## Data Flow & Storage
736736+737737+### Like Flow
738738+739739+```
740740+User clicks "Like" on a brew
741741+ ↓
742742+Frontend sends POST /api/likes
743743+ ↓
744744+Backend creates app.bsky.feed.like record in user's PDS
745745+ ↓
746746+PDS broadcasts record to Relay via firehose
747747+ ↓
748748+Arabica firehose listener receives event
749749+ ↓
750750+SocialIndex updates like count for subject URI
751751+ ↓
752752+Cache invalidated (optional)
753753+ ↓
754754+Feed refreshes with new like count
755755+```
756756+757757+### Follow Flow
758758+759759+```
760760+User clicks "Follow" on profile
761761+ ↓
762762+Frontend sends POST /api/follows
763763+ ↓
764764+Backend creates app.bsky.graph.follow record in user's PDS
765765+ ↓
766766+PDS broadcasts to firehose
767767+ ↓
768768+Arabica listener updates FollowsIndex
769769+ ↓
770770+User's feed now includes followed user's brews
771771+```
772772+773773+### Comment Flow
774774+775775+```
776776+User submits comment on brew
777777+ ↓
778778+Frontend sends POST /api/comments
779779+ ↓
780780+Backend creates social.arabica.alpha.comment in user's PDS
781781+ ↓
782782+Firehose broadcasts event
783783+ ↓
784784+Arabica listener indexes comment
785785+ ↓
786786+Comment appears on brew detail page
787787+```
788788+789789+---
790790+791791+## UI/UX Considerations
792792+793793+### Like Button
794794+795795+**States:**
796796+797797+- Not liked: Gray heart outline
798798+- Liked: Red filled heart
799799+- Loading: Gray heart with spinner
800800+801801+**Display:**
802802+803803+- Show like count next to heart
804804+- On hover: Show "X people liked this"
805805+- Click: Optimistic update (instant UI change, API call in background)
806806+807807+**Location:**
808808+809809+- Brew cards in feed
810810+- Brew detail page
811811+- Bean/Roaster/Grinder/Brewer detail pages
812812+813813+### Follow Button
814814+815815+**States:**
816816+817817+- Not following: "Follow" button (blue)
818818+- Following: "Following" button (gray, checkmark)
819819+- Hover over "Following": "Unfollow" (red)
820820+821821+**Location:**
822822+823823+- User profile header
824824+- Brew author byline (small follow button)
825825+- Followers/Following lists
826826+827827+### Comments Section
828828+829829+**Layout:**
830830+831831+- Threaded comments (indented replies)
832832+- Show comment count
833833+- "Load more" pagination (20 per page)
834834+- Sort by: Newest, Oldest, Most Liked
835835+836836+**Comment Form:**
837837+838838+- Textarea with mention support (@username autocomplete)
839839+- Optional rating (1-10 stars)
840840+- Cancel/Submit buttons
841841+- Character count (500 max)
842842+843843+---
844844+845845+## Migration & Rollout
846846+847847+### Step 1: Backend Deployment (Week 1)
848848+849849+1. Deploy firehose listener with like/follow indexing
850850+2. Backfill existing likes/follows from firehose history
851851+3. Test API endpoints in staging
852852+4. Monitor BoltDB storage growth
853853+854854+### Step 2: Frontend Soft Launch (Week 2)
855855+856856+1. Deploy like button (feature flag: enabled for beta users)
857857+2. Collect feedback
858858+3. Fix bugs
859859+860860+### Step 3: Public Launch (Week 3)
861861+862862+1. Enable likes for all users
863863+2. Announce on Bluesky: "You can now like brews on Arabica!"
864864+3. Monitor server load
865865+866866+### Step 4: Follow Feature (Week 4-5)
867867+868868+1. Deploy follow indexing
869869+2. Add follow button to profiles
870870+3. Add "Following" feed filter
871871+4. Launch import-from-Bluesky tool
872872+873873+### Step 5: Comments (Week 6-8)
874874+875875+1. Define and publish comment lexicon
876876+2. Deploy comment indexing
877877+3. Add comment UI
878878+4. Test threading
879879+880880+---
881881+882882+## Future Enhancements
883883+884884+### Phase 5+: Advanced Social Features
885885+886886+1. **Notifications**
887887+ - "X liked your brew"
888888+ - "Y commented on your brew"
889889+ - WebSocket-based real-time updates
890890+891891+2. **Social Discovery**
892892+ - "Trending brews this week"
893893+ - "Popular roasters"
894894+ - "Top coffee influencers"
895895+896896+3. **Activity Feed**
897897+ - "Your friend Alice brewed a new espresso"
898898+ - "Bob rated a bean you liked"
899899+900900+4. **Lists & Collections**
901901+ - "My favorite light roasts" (curated bean list)
902902+ - "Seattle coffee shops" (location-based)
903903+904904+5. **Collaborative Brewing**
905905+ - Share brew recipes
906906+ - Clone someone's brew with credit
907907+ - Brew challenges ("30 days of pour-over")
908908+909909+6. **Cross-App Features**
910910+ - Share brew to Bluesky as a post (with photo)
911911+ - Embed brew cards in Bluesky posts
912912+ - "Post to Bluesky" button on brew creation
913913+914914+---
915915+916916+## Appendix A: Lexicon Files
917917+918918+### Proposed: `social.arabica.alpha.like.json` (NOT RECOMMENDED)
919919+920920+If we decide NOT to use `app.bsky.feed.like`, here's our custom lexicon:
921921+922922+```json
923923+{
924924+ "lexicon": 1,
925925+ "id": "social.arabica.alpha.like",
926926+ "defs": {
927927+ "main": {
928928+ "type": "record",
929929+ "key": "tid",
930930+ "description": "A like on a brew, bean, roaster, grinder, or brewer",
931931+ "record": {
932932+ "type": "object",
933933+ "required": ["subject", "createdAt"],
934934+ "properties": {
935935+ "subject": {
936936+ "type": "ref",
937937+ "ref": "com.atproto.repo.strongRef",
938938+ "description": "The record being liked"
939939+ },
940940+ "createdAt": {
941941+ "type": "string",
942942+ "format": "datetime"
943943+ }
944944+ }
945945+ }
946946+ }
947947+ }
948948+}
949949+```
950950+951951+### Proposed: `social.arabica.alpha.follow.json` (NOT RECOMMENDED)
952952+953953+```json
954954+{
955955+ "lexicon": 1,
956956+ "id": "social.arabica.alpha.follow",
957957+ "defs": {
958958+ "main": {
959959+ "type": "record",
960960+ "key": "tid",
961961+ "description": "Following another coffee enthusiast",
962962+ "record": {
963963+ "type": "object",
964964+ "required": ["subject", "createdAt"],
965965+ "properties": {
966966+ "subject": {
967967+ "type": "string",
968968+ "format": "did",
969969+ "description": "DID of the user being followed"
970970+ },
971971+ "createdAt": {
972972+ "type": "string",
973973+ "format": "datetime"
974974+ }
975975+ }
976976+ }
977977+ }
978978+ }
979979+}
980980+```
981981+982982+---
983983+984984+## Appendix B: Estimated Effort
985985+986986+| Phase | Feature | Backend | Frontend | Testing | Total |
987987+| --------- | ----------------- | ------- | -------- | ------- | ------------------------ |
988988+| 1 | Likes | 5 days | 3 days | 2 days | **10 days** |
989989+| 2 | Follows | 7 days | 5 days | 3 days | **15 days** |
990990+| 3 | Comments | 8 days | 6 days | 4 days | **18 days** |
991991+| 4 | Feed Enhancements | 4 days | 4 days | 2 days | **10 days** |
992992+| **Total** | | | | | **53 days (10.6 weeks)** |
993993+994994+---
995995+996996+## Appendix C: Open Questions
997997+998998+1. **Moderation**: How do we handle spam comments or abusive likes?
999999+ - Use AT Protocol's label system?
10001000+ - Admin moderation tools?
10011001+10021002+2. **Privacy**: Should follows be private?
10031003+ - Current plan: Public (like Bluesky)
10041004+ - Could add private follows later
10051005+10061006+3. **Notifications**: What delivery mechanism?
10071007+ - WebSocket for real-time?
10081008+ - Polling API?
10091009+ - Email digests?
10101010+10111011+4. **Analytics**: Track engagement metrics?
10121012+ - Like/comment rates
10131013+ - User retention
10141014+ - Popular content
10151015+10161016+5. **Mobile**: When to build native apps?
10171017+ - After web is stable
10181018+ - Consider PWA first
10191019+10201020+---
10211021+10221022+## Conclusion
10231023+10241024+This plan provides a **phased, pragmatic approach** to adding social features to Arabica. By **reusing Bluesky's `like` and `follow` lexicons**, we gain:
10251025+10261026+- ✅ Cross-app discoverability
10271027+- ✅ Simpler implementation
10281028+- ✅ Follow import from Bluesky
10291029+- ✅ Future-proof social graph
10301030+10311031+While **custom comments** allow:
10321032+10331033+- ✅ Coffee-specific features (ratings, tasting notes)
10341034+- ✅ Cleaner separation from Bluesky threads
10351035+- ✅ Control over threading UX
10361036+10371037+**Next Steps:**
10381038+10391039+1. Review and approve this plan
10401040+2. Begin Phase 1 (Likes) implementation
10411041+3. Iterate based on user feedback
10421042+4. Expand to follows and comments
10431043+10441044+**Timeline:** ~11 weeks for all phases (with 1 developer)
10451045+10461046+---
10471047+10481048+**Document Status:** Draft for Review
10491049+**Last Updated:** January 25, 2026
10501050+**Author:** AI Assistant (with human review pending)
···175175 <p class="text-brown-800 leading-relaxed">
176176 Arabica is open source software. You can view the code, contribute, or
177177 even run your own instance. Visit our <a
178178- href="https://github.com/ptdewey/arabica"
178178+ href="https://tangled/arabica.social/arabica"
179179 class="text-brown-700 hover:underline font-medium"
180180 target="_blank"
181181- rel="noopener noreferrer">GitHub repository</a
181181+ rel="noopener noreferrer">Tangled repository</a
182182 > to learn more.
183183 </p>
184184 </section>
···11+[private]
22+default: build-ui run
33+14run:
25 @LOG_LEVEL=debug LOG_FORMAT=console go run cmd/arabica-server/main.go -known-dids known-dids.txt
36