···11-# Arabica AT Protocol Migration Plan
22-33-## Overview
44-55-This document outlines the plan to transform Arabica from a self-hosted SQLite-based coffee tracking application into a decentralized AT Protocol (atproto) application where user data lives in their Personal Data Servers (PDS).
66-77-## Project Goals
88-99-### Core Principles
1010-1111-- **Decentralized Storage**: User data lives in their own PDS, not our server
1212-- **Public Records**: All coffee tracking records are public via atproto repo exploration
1313-- **Self-hostable AppView**: Our server is the primary AppView, but others can self-host
1414-- **OAuth Authentication**: Users authenticate via atproto OAuth with scopes (not app passwords)
1515-- **Backward Compatible UX**: Preserve the user-friendly interface and workflow
1616-1717-### Architecture Vision
1818-1919-```
2020-┌─────────────────────────────────────────────────────────────┐
2121-│ Arabica AppView (Go Server) │
2222-│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
2323-│ │ Web UI │ │ OAuth │ │ atproto │ │
2424-│ │ (HTMX/HTML) │ │ (indigo) │ │ Client │ │
2525-│ └──────────────┘ └──────────────┘ └──────────────────┘ │
2626-│ │
2727-│ ┌──────────────────────────────────────────────────────┐ │
2828-│ │ Future: AppView Index & Discovery │ │
2929-│ │ - Firehose subscriber │ │
3030-│ │ - Cross-user search & discovery │ │
3131-│ │ - Social features │ │
3232-│ └──────────────────────────────────────────────────────┘ │
3333-└─────────────────────────────────────────────────────────────┘
3434- │
3535- ↓ OAuth + XRPC
3636- ┌───────────────────────────┐
3737- │ User's PDS (any PDS) │
3838- │ at://did:plc:abc123/ │
3939- │ ├── com.arabica.brew │
4040- │ ├── com.arabica.bean │
4141- │ ├── com.arabica.roaster │
4242- │ ├── com.arabica.grinder │
4343- │ └── com.arabica.brewer │
4444- └───────────────────────────┘
4545-```
4646-4747----
4848-4949-## Phase 0: Research & Validation (2-3 days)
5050-5151-### Goals
5252-5353-- Understand `indigo` SDK capabilities
5454-- Validate lexicon schemas
5555-- Set up development environment
5656-- Test basic atproto operations
5757-5858-### Tasks
5959-6060-#### 0.1: Research `indigo` SDK
6161-6262-- [ ] Review `github.com/bluesky-social/indigo` documentation
6363-- [ ] Study OAuth implementation in indigo (with scopes)
6464-- [ ] Understand XRPC client usage
6565-- [ ] Review record CRUD operations API
6666-- [ ] Find example applications using indigo
6767-- [ ] Document findings in `docs/indigo-research.md`
6868-6969-**Key Questions:**
7070-7171-- Does indigo provide complete OAuth client with DPOP support?
7272-- What's the API for repo operations (createRecord, getRecord, listRecords, etc.)?
7373-- Are there session management helpers?
7474-- How are AT-URIs parsed and resolved?
7575-7676-#### 0.2: Design & Validate Lexicons
7777-7878-- [ ] Write lexicon JSON files for all record types:
7979- - `com.arabica.bean`
8080- - `com.arabica.roaster`
8181- - `com.arabica.grinder`
8282- - `com.arabica.brewer`
8383- - `com.arabica.brew`
8484-- [ ] Validate lexicons against atproto schema validator
8585-- [ ] Document schema decisions in `docs/schema-design.md`
8686-- [ ] Review field mappings from current SQLite schema
8787-8888-**Key Decisions:**
8989-9090-- Reference format: AT-URIs vs embedded objects
9191-- Pours: Embedded in brew record vs separate collection
9292-- Enums vs free text for fields like `method`, `roastLevel`, etc.
9393-- String length limits for all text fields
9494-9595-#### 0.3: Set up Development Environment
9696-9797-- [ ] Create/use Bluesky account for testing (or run local PDS)
9898-- [ ] Install atproto development tools (CLI, validators)
9999-- [ ] Set up test DID for development
100100-- [ ] Configure environment variables for PDS endpoints
101101-102102-**Options:**
103103-104104-- **Option A:** Use Bluesky PDS (bsky.social) - easiest, most realistic
105105-- **Option B:** Run local PDS via Docker - more control, offline dev
106106-- **Recommended:** Option A for initial development
107107-108108-#### 0.4: Manual Record Creation Test
109109-110110-- [ ] Manually create test records using atproto CLI or tools
111111-- [ ] Test all 5 record types (bean, roaster, grinder, brewer, brew)
112112-- [ ] Test cross-references (brew → bean → roaster)
113113-- [ ] Verify records appear in repo explorer
114114-- [ ] Query records back via API
115115-- [ ] Document any issues or schema adjustments needed
116116-117117-**Deliverables:**
118118-119119-- Working lexicon files in `lexicons/` directory
120120-- Test records in development PDS
121121-- Research notes documenting indigo SDK usage
122122-123123----
124124-125125-## Phase 1: Core atproto Client Layer (3-4 days)
126126-127127-### Goals
128128-129129-- Integrate `indigo` SDK
130130-- Create atproto client wrapper
131131-- Implement new Store interface using PDS operations
132132-- Replace SQLite dependency with atproto records
133133-134134-### Tasks
135135-136136-#### 1.1: Project Structure Setup
137137-138138-- [ ] Add `github.com/bluesky-social/indigo` to go.mod
139139-- [ ] Create new package structure:
140140- ```
141141- internal/
142142- ├── atproto/
143143- │ ├── client.go # XRPC client wrapper
144144- │ ├── records.go # Record CRUD operations
145145- │ ├── resolver.go # AT-URI and reference resolution
146146- │ └── store.go # Store interface implementation
147147- ```
148148-- [ ] Keep `internal/database/store.go` interface (for compatibility)
149149-- [ ] Move lexicon files to `lexicons/` at project root
150150-151151-#### 1.2: atproto Client Wrapper (`internal/atproto/client.go`)
152152-153153-Create a wrapper around indigo's XRPC client with high-level operations:
154154-155155-```go
156156-type Client struct {
157157- xrpc *xrpc.Client
158158- pdsURL string
159159-}
160160-161161-func NewClient(pdsURL string) (*Client, error)
162162-```
163163-164164-**Core Methods:**
165165-166166-- [ ] `CreateRecord(did, collection, record)` → (rkey, error)
167167-- [ ] `GetRecord(did, collection, rkey)` → (record, error)
168168-- [ ] `ListRecords(did, collection, limit, cursor)` → (records, cursor, error)
169169-- [ ] `PutRecord(did, collection, rkey, record)` → error
170170-- [ ] `DeleteRecord(did, collection, rkey)` → error
171171-172172-**Helper Methods:**
173173-174174-- [ ] `ResolveATURI(uri string)` → (did, collection, rkey, error)
175175-- [ ] `BuildATURI(did, collection, rkey)` → string
176176-- [ ] Error handling and retry logic
177177-178178-#### 1.3: Record Type Mapping (`internal/atproto/records.go`)
179179-180180-Map Go structs to/from atproto records:
181181-182182-```go
183183-// Convert domain models to atproto records
184184-func BrewToRecord(brew *models.Brew) (map[string]interface{}, error)
185185-func RecordToBrew(record map[string]interface{}) (*models.Brew, error)
186186-187187-// Similar for Bean, Roaster, Grinder, Brewer
188188-```
189189-190190-**Considerations:**
191191-192192-- [ ] Handle time.Time → RFC3339 string conversion
193193-- [ ] Build AT-URI references for foreign keys
194194-- [ ] Validate required fields before sending to PDS
195195-- [ ] Handle optional fields (nil pointers)
196196-197197-#### 1.4: Reference Resolution (`internal/atproto/resolver.go`)
198198-199199-Handle fetching referenced records:
200200-201201-```go
202202-// Resolve a single reference
203203-func (c *Client) ResolveReference(atURI string, target interface{}) error
204204-205205-// Batch resolve multiple references (optimization for later)
206206-func (c *Client) BatchResolve(atURIs []string) (map[string]interface{}, error)
207207-```
208208-209209-**Strategy (Phase 1):**
210210-211211-- Start with simple lazy loading (one request per reference)
212212-- Document optimization opportunities for later
213213-214214-#### 1.5: Store Implementation (`internal/atproto/store.go`)
215215-216216-Implement the existing `database.Store` interface using atproto operations:
217217-218218-```go
219219-type AtprotoStore struct {
220220- client *Client
221221- did string // Current authenticated user's DID
222222-}
223223-224224-func NewAtprotoStore(client *Client, did string) *AtprotoStore
225225-```
226226-227227-**Implement all Store methods:**
228228-229229-- [ ] Brew operations (CreateBrew, GetBrew, ListBrews, UpdateBrew, DeleteBrew)
230230-- [ ] Bean operations
231231-- [ ] Roaster operations
232232-- [ ] Grinder operations
233233-- [ ] Brewer operations
234234-- [ ] Pour operations (embedded in brews, special handling)
235235-236236-**Key Changes from SQLite:**
237237-238238-- No user_id field (implicit from DID)
239239-- References stored as AT-URIs
240240-- Need to resolve references when displaying
241241-- List operations may need pagination handling
242242-243243-#### 1.6: Testing
11+# Implementation Notes
2442245245-- [ ] Unit tests for client operations (against test PDS)
246246-- [ ] Test record conversion functions
247247-- [ ] Test reference resolution
248248-- [ ] Test Store interface implementation
249249-- [ ] Integration test: full CRUD cycle for each record type
33+## Current Status
2504251251-**Deliverables:**
55+Arabica is a coffee tracking web application using AT Protocol for decentralized data storage.
2526253253-- Working atproto client layer
254254-- Store interface implemented via PDS
255255-- SQLite can be completely removed (but keep for sessions - see Phase 2)
256256-257257----
258258-259259-## Phase 2: Authentication & OAuth (3-4 days)
260260-261261-### Goals
262262-263263-- Implement atproto OAuth flow using indigo (with scopes, not app passwords)
264264-- Add session management
265265-- Add authentication middleware
266266-- Update UI for login/logout
267267-268268-### Tasks
269269-270270-#### 2.1: OAuth Server Setup (`internal/atproto/oauth.go`)
271271-272272-Implement OAuth using **indigo's OAuth client** (preferred approach):
273273-274274-**IMPORTANT:** Use OAuth scopes, NOT app passwords. This provides proper security and user control.
275275-276276-```go
277277-type OAuthHandler struct {
278278- // Use indigo's OAuth client
279279- oauthClient *indigo_oauth.Client
280280- clientID string
281281- redirectURI string
282282- scopes []string // Standard: ["atproto", "transition:generic"]
283283-}
284284-```
285285-286286-**OAuth Client Metadata (Required for Registration):**
287287-You'll need to register your OAuth client with atproto. Required metadata:
288288-289289-- `client_id`: Your application identifier (e.g., `https://arabica.example.com/client-metadata.json`)
290290-- `client_name`: "Arabica Coffee Tracker"
291291-- `client_uri`: Your app homepage URL
292292-- `redirect_uris`: Array of callback URLs (e.g., `["https://arabica.example.com/oauth/callback"]`)
293293-- `scope`: Space-separated scopes (e.g., `"atproto transition:generic"`)
294294-- `grant_types`: `["authorization_code", "refresh_token"]`
295295-- `response_types`: `["code"]`
296296-- `token_endpoint_auth_method`: `"none"` (for public clients)
297297-- `application_type`: `"web"`
298298-- `dpop_bound_access_tokens`: `true` (REQUIRED - enables DPOP)
77+**Completed:**
88+- OAuth authentication with AT Protocol
99+- Record CRUD operations for all entity types
1010+- Community feed from registered users
1111+- BoltDB for session persistence and feed registry
1212+- Mobile-friendly UI with HTMX
29913300300-**Client Metadata Hosting:**
1414+## Architecture
30115302302-- [ ] Serve client metadata at `/.well-known/oauth-client-metadata` or at your `client_id` URL
303303-- [ ] Must be publicly accessible HTTPS endpoint
304304-- [ ] Content-Type: `application/json`
1616+### Data Storage
1717+- User data: AT Protocol Personal Data Servers
1818+- Sessions: BoltDB (local)
1919+- Feed registry: BoltDB (local)
30520306306-**Required Endpoints:**
2121+### Record Types
2222+- `social.arabica.alpha.bean` - Coffee beans
2323+- `social.arabica.alpha.roaster` - Roasters
2424+- `social.arabica.alpha.grinder` - Grinders
2525+- `social.arabica.alpha.brewer` - Brewing devices
2626+- `social.arabica.alpha.brew` - Brew sessions
30727308308-- [ ] `GET /login` - Initiate OAuth flow (handle → PDS discovery → auth endpoint)
309309-- [ ] `GET /oauth/callback` - Handle OAuth callback with authorization code
310310-- [ ] `POST /logout` - Clear session and revoke tokens
2828+### Key Components
2929+- `internal/atproto/` - AT Protocol client and OAuth
3030+- `internal/handlers/` - HTTP request handlers
3131+- `internal/bff/` - Template rendering layer
3232+- `internal/feed/` - Community feed service
3333+- `internal/database/boltstore/` - BoltDB persistence
31134312312-**OAuth Flow with indigo:**
3535+## Future Improvements
31336314314-1. User enters their handle (e.g., `alice.bsky.social`)
315315-2. Resolve handle → DID → PDS URL
316316-3. Discover PDS OAuth endpoints (authorization_endpoint, token_endpoint)
317317-4. Generate PKCE challenge and state
318318-5. Build authorization URL with:
319319- - `client_id`
320320- - `redirect_uri`
321321- - `scope` (e.g., `"atproto transition:generic"`)
322322- - `response_type=code`
323323- - `code_challenge` and `code_challenge_method=S256` (PKCE)
324324- - `state` (CSRF protection)
325325-6. Redirect user to PDS authorization endpoint
326326-7. User authorizes on their PDS
327327-8. PDS redirects back with authorization code
328328-9. Exchange code for tokens using **DPOP**:
329329- - Generate DPOP proof JWT
330330- - POST to token_endpoint with code, PKCE verifier, and DPOP proof
331331- - Receive access_token, refresh_token (both DPOP-bound)
332332-10. Store session with DID, tokens, and DPOP key
3737+### Performance
3838+- Implement firehose subscriber for real-time feed updates
3939+- Add caching layer for frequently accessed records
4040+- Optimize parallel record fetching
33341334334-**Key Implementation Details:**
4242+### Features
4343+- Search and filtering
4444+- User profiles and following
4545+- Recipe sharing
4646+- Statistics and analytics
33547336336-- [ ] Use indigo's OAuth client library (handles DPOP automatically)
337337-- [ ] Generate and store DPOP keypairs per session
338338-- [ ] All PDS API calls must include DPOP proof header
339339-- [ ] Handle token refresh (also requires DPOP)
340340-- [ ] Support multiple PDS providers (not just Bluesky)
341341-- [ ] Handle resolution and DID validation
342342-- [ ] PKCE for additional security
343343-344344-**indigo OAuth Components to Use:**
345345-346346-- Handle → DID resolution
347347-- PDS → OAuth endpoint discovery
348348-- DPOP key generation and proof creation
349349-- Token exchange with DPOP
350350-- Token refresh with DPOP
351351-352352-**Security Considerations:**
353353-354354-- [ ] Validate `state` parameter to prevent CSRF
355355-- [ ] Verify PKCE code_verifier matches challenge
356356-- [ ] Store DPOP private keys securely (encrypted in session)
357357-- [ ] Use HTTP-only, secure cookies for session ID
358358-- [ ] Implement token expiration checking
359359-- [ ] Revoke tokens on logout
360360-361361-#### 2.2: Session Management
362362-363363-Store authenticated sessions with user DID and tokens.
364364-365365-**Options:**
366366-367367-- **Development:** In-memory map (simple, ephemeral)
368368-- **Production:** Redis or SQLite for sessions
369369-370370-**Decision (to be made in implementation):**
371371-372372-- Start with in-memory for development
373373-- Document production session storage strategy
374374-- Use secure HTTP-only cookies for session ID
375375-376376-**Session Structure:**
377377-378378-```go
379379-type Session struct {
380380- SessionID string
381381- DID string
382382- AccessToken string
383383- RefreshToken string
384384- ExpiresAt time.Time
385385- CreatedAt time.Time
386386-}
387387-```
388388-389389-**Required Methods:**
390390-391391-- [ ] `CreateSession(did, tokens)` → sessionID
392392-- [ ] `GetSession(sessionID)` → session
393393-- [ ] `DeleteSession(sessionID)`
394394-- [ ] `RefreshSession(sessionID)` → updated session
395395-396396-#### 2.3: Authentication Middleware
397397-398398-Add middleware to extract authenticated user from session:
399399-400400-```go
401401-func AuthMiddleware(next http.Handler) http.Handler
402402-403403-// Context key for authenticated DID
404404-type contextKey string
405405-const userDIDKey contextKey = "userDID"
406406-407407-// Helper to get DID from context
408408-func GetAuthenticatedDID(r *http.Request) (string, error)
409409-```
410410-411411-**Behavior:**
412412-413413-- Extract session cookie
414414-- Validate session exists and not expired
415415-- Add DID to request context
416416-- If invalid/missing: redirect to login (for protected routes)
417417-418418-**Route Protection:**
419419-420420-- [ ] Protected routes: All write operations (POST, PUT, DELETE)
421421-- [ ] Public routes: Home page, static assets
422422-- [ ] Semi-protected: Brew list (show your own if logged in, or empty state)
423423-424424-#### 2.4: UI Updates for Authentication
425425-426426-Update templates to support authentication:
427427-428428-**New Templates:**
429429-430430-- [ ] `login.tmpl` - Login page with OAuth button
431431-- [ ] Update `layout.tmpl` - Add user info header
432432- - If logged in: Show handle, logout button
433433- - If logged out: Show login button
434434-435435-**Navigation Updates:**
436436-437437-- [ ] Add user menu/dropdown
438438-- [ ] Link to profile/settings (future)
439439-- [ ] Display current user's handle
440440-441441-**Empty States:**
442442-443443-- [ ] If not logged in: Show welcome page with login prompt
444444-- [ ] If logged in but no data: Show getting started guide
445445-446446-#### 2.5: Handler Updates
447447-448448-Update handlers to use authenticated DID:
449449-450450-```go
451451-func (h *Handler) HandleBrewCreate(w http.ResponseWriter, r *http.Request) {
452452- // Get authenticated user's DID
453453- did, err := GetAuthenticatedDID(r)
454454- if err != nil {
455455- http.Error(w, "Unauthorized", http.StatusUnauthorized)
456456- return
457457- }
458458-459459- // Create store scoped to this user
460460- store := atproto.NewAtprotoStore(h.client, did)
461461-462462- // Rest of handler logic...
463463-}
464464-```
465465-466466-**All handlers need:**
467467-468468-- [ ] Extract DID from context
469469-- [ ] Create user-scoped store
470470-- [ ] Handle unauthenticated users gracefully
471471-472472-#### 2.6: Configuration
473473-474474-Add environment variables for OAuth:
475475-476476-```bash
477477-# OAuth Configuration
478478-OAUTH_CLIENT_ID=your_client_id
479479-OAUTH_CLIENT_SECRET=your_client_secret
480480-OAUTH_REDIRECT_URI=http://localhost:8080/oauth/callback
481481-OAUTH_SCOPES=atproto,transition:generic
482482-483483-# PDS Configuration
484484-DEFAULT_PDS_URL=https://bsky.social # or support dynamic discovery
485485-486486-# Session Configuration
487487-SESSION_SECRET=random_secret_key
488488-SESSION_MAX_AGE=86400 # 24 hours
489489-```
490490-491491-**Deliverables:**
492492-493493-- Working OAuth login flow
494494-- Session management
495495-- Protected routes with authentication
496496-- Updated UI with login/logout
497497-- Users can authenticate and access their own data
498498-499499----
500500-501501-## Phase 3: Handler & UI Refactoring (2-3 days)
502502-503503-### Goals
504504-505505-- Update all handlers to work with atproto store
506506-- Handle reference resolution in UI
507507-- Remove SQLite dependency
508508-- Improve error handling
509509-510510-### Tasks
511511-512512-#### 3.1: Update All Handlers
513513-514514-Modify handlers to use atproto store with authenticated DID:
515515-516516-**Handlers to update:**
517517-518518-- [ ] `HandleBrewList` - List user's brews with resolved references
519519-- [ ] `HandleBrewNew` - Create new brew
520520-- [ ] `HandleBrewEdit` - Edit existing brew
521521-- [ ] `HandleBrewCreate` - Process create form
522522-- [ ] `HandleBrewUpdate` - Process update form
523523-- [ ] `HandleBrewDelete` - Delete brew
524524-- [ ] `HandleBrewExport` - Export from PDS (not SQLite)
525525-- [ ] Bean CRUD handlers (create, update, delete)
526526-- [ ] Roaster CRUD handlers
527527-- [ ] Grinder CRUD handlers
528528-- [ ] Brewer CRUD handlers
529529-- [ ] `HandleManage` - Manage all resources
530530-531531-**Key Changes:**
532532-533533-- Get DID from request context
534534-- Create user-scoped store
535535-- Handle references (AT-URIs) in forms
536536-- Resolve references for display
537537-- Error handling for PDS operations
538538-539539-#### 3.2: Reference Handling in UI
540540-541541-Update forms and displays to handle AT-URI references:
542542-543543-**Brew Form Updates:**
544544-545545-- [ ] Bean selector: Show user's beans, store AT-URI
546546-- [ ] Roaster selector: Show user's roasters (via bean's roasterRef)
547547-- [ ] Grinder selector: Show user's grinders, store AT-URI
548548-- [ ] Brewer selector: Show user's brewers, store AT-URI
549549-550550-**Display Updates:**
551551-552552-- [ ] Resolve bean reference when displaying brew
553553-- [ ] Resolve grinder reference
554554-- [ ] Resolve brewer reference
555555-- [ ] Show resolved data (bean name, roaster name, etc.)
556556-- [ ] Handle broken references gracefully (show "Unknown" or ID)
557557-558558-**New Bean Form:**
559559-560560-- [ ] Roaster selector stores AT-URI reference
561561-- [ ] Create new roaster inline (stores as new record, returns AT-URI)
562562-563563-#### 3.3: Error Handling
564564-565565-Improve error handling for atproto operations:
566566-567567-**PDS Operation Errors:**
568568-569569-- [ ] Network failures - retry with backoff
570570-- [ ] Authentication failures - redirect to login
571571-- [ ] Rate limiting - show user-friendly message
572572-- [ ] Invalid records - show validation errors
573573-- [ ] Missing references - handle gracefully
574574-575575-**User-Friendly Error Pages:**
576576-577577-- [ ] 401 Unauthorized - redirect to login
578578-- [ ] 404 Not Found - "Record not found" page
579579-- [ ] 500 Server Error - "Something went wrong" with error ID
580580-- [ ] PDS Unavailable - "Cannot connect to your PDS" message
581581-582582-**Logging:**
583583-584584-- [ ] Log all PDS operations
585585-- [ ] Log OAuth flow steps
586586-- [ ] Log errors with context (DID, operation, error)
587587-- [ ] Add request ID for tracking
588588-589589-#### 3.4: Remove SQLite Dependency
590590-591591-- [ ] Remove `internal/database/sqlite/` package
592592-- [ ] Remove SQLite imports from main.go
593593-- [ ] Remove database migrations
594594-- [ ] Update go.mod (remove modernc.org/sqlite)
595595-- [ ] Update README (remove SQLite references)
596596-597597-**Note:** May keep SQLite or add Redis for session storage in production (TBD).
598598-599599-#### 3.5: Update PWA/Offline Handling
600600-601601-Since app now requires online access to PDS:
602602-603603-**Options:**
604604-605605-- [ ] **Option A:** Remove service worker and PWA manifest (simple)
606606-- [ ] **Option B:** Keep PWA but update service worker to only cache static assets
607607-- [ ] **Option C:** Add offline queue for writes (complex, future enhancement)
608608-609609-**Recommendation:** Option B - keep PWA for "Add to Home Screen" but require online for data.
610610-611611-#### 3.6: Testing
612612-613613-- [ ] Manual testing of full user flow:
614614- 1. Login with OAuth
615615- 2. Create beans, roasters, grinders, brewers
616616- 3. Create brews with references
617617- 4. Edit brews
618618- 5. Delete brews
619619- 6. Logout and verify data persists (in PDS)
620620- 7. Login again and verify data loads
621621-- [ ] Test error scenarios (network failures, invalid data, etc.)
622622-- [ ] Test with multiple users (different DIDs)
623623-624624-**Deliverables:**
625625-626626-- Fully functional personal coffee tracker using atproto
627627-- All CRUD operations working
628628-- Reference resolution working
629629-- SQLite removed
630630-- Error handling improved
631631-632632----
633633-634634-## Phase 4: Polish & Documentation (2-3 days)
635635-636636-### Goals
637637-638638-- Production-ready configuration
639639-- Deployment documentation
640640-- User documentation
641641-- Clean up code
642642-643643-### Tasks
644644-645645-#### 4.1: Production Configuration
646646-647647-- [ ] Environment variable validation on startup
648648-- [ ] Configuration file support (optional)
649649-- [ ] Secure session storage (Redis or encrypted SQLite)
650650-- [ ] HTTPS/TLS configuration
651651-- [ ] Rate limiting for API endpoints
652652-- [ ] CORS configuration
653653-654654-#### 4.2: Deployment Preparation
655655-656656-- [ ] Create Dockerfile
657657-- [ ] Docker compose for development (app + Redis)
658658-- [ ] Document environment variables
659659-- [ ] Health check endpoint (`/health`)
660660-- [ ] Graceful shutdown handling
661661-- [ ] Production logging configuration
662662-663663-#### 4.3: Documentation
664664-665665-- [ ] Update README.md:
666666- - New architecture overview
667667- - atproto concepts
668668- - Setup instructions
669669- - Environment variables
670670- - Deployment guide
671671-- [ ] Create DEPLOYMENT.md:
672672- - Server requirements
673673- - OAuth app registration
674674- - Domain/DNS setup
675675- - SSL certificate setup
676676- - Systemd service example
677677-- [ ] Create SELF-HOSTING.md:
678678- - Guide for others to run their own AppView
679679- - Configuration options
680680- - Maintenance tasks
681681-- [ ] Create docs/LEXICONS.md:
682682- - Document all lexicon schemas
683683- - Field descriptions
684684- - Reference patterns
685685- - Examples
686686-687687-#### 4.4: Code Cleanup
688688-689689-- [ ] Add/improve code comments
690690-- [ ] Consistent error handling patterns
691691-- [ ] Extract magic constants to config
692692-- [ ] Remove dead code
693693-- [ ] Format all code (gofmt)
694694-- [ ] Lint and fix issues (golangci-lint)
695695-696696-#### 4.5: Testing
697697-698698-- [ ] Write unit tests for critical paths
699699-- [ ] Integration tests for OAuth flow
700700-- [ ] Test deployment in production-like environment
701701-- [ ] Load testing (basic)
702702-- [ ] Security review (OWASP basics)
703703-704704-**Deliverables:**
705705-706706-- Production-ready application
707707-- Complete documentation
708708-- Deployment artifacts (Docker, etc.)
709709-- Ready to launch
710710-711711----
712712-713713-## Phase 5: Launch & Initial Users (TBD)
714714-715715-### Goals
716716-717717-- Deploy to production
718718-- Onboard initial users
719719-- Monitor and fix issues
720720-- Gather feedback
721721-722722-### Tasks
723723-724724-- [ ] Deploy to production hosting
725725-- [ ] Set up monitoring and alerting
726726-- [ ] Create landing page explaining the project
727727-- [ ] Announce in atproto community
728728-- [ ] Gather user feedback
729729-- [ ] Fix bugs and usability issues
730730-- [ ] Iterate based on feedback
731731-732732-**Success Metrics:**
733733-734734-- Users can successfully authenticate
735735-- Users can create and manage their coffee data
736736-- Data persists in their PDS
737737-- No critical bugs
738738-739739----
740740-741741-## Future Enhancements (Post-Launch)
742742-743743-### Phase 6: Public Browsing & Discovery (Future)
744744-745745-**Goal:** Allow users to discover and view other users' coffee brews.
746746-747747-**Features:**
748748-749749-- [ ] Public profile pages (`/users/:did`)
750750-- [ ] Browse any user's public brews
751751-- [ ] Manual DID entry for discovery
752752-- [ ] Handle resolution (DID → handle display)
753753-- [ ] Basic search within a user's data
754754-755755-**Technical:**
756756-757757-- [ ] Query any PDS for public records
758758-- [ ] Cache results for performance
759759-- [ ] Handle PDS availability issues
760760-761761-### Phase 7: AppView Indexing (Future)
762762-763763-**Goal:** Build a centralized index for cross-user discovery.
764764-765765-**Features:**
766766-767767-- [ ] Firehose subscription
768768-- [ ] Index public arabica records from all PDSs
769769-- [ ] Cross-user search (by bean, roaster, method, etc.)
770770-- [ ] Trending/popular content
771771-- [ ] User directory
772772-773773-**Technical:**
774774-775775-- [ ] Add PostgreSQL for index storage
776776-- [ ] Firehose consumer using indigo
777777-- [ ] Background indexing jobs
778778-- [ ] Search API
779779-780780-### Phase 8: Social Features (Future)
781781-782782-**Goal:** Add social interactions around coffee.
783783-784784-**Features:**
785785-786786-- [ ] Follow users
787787-- [ ] Like/bookmark brews
788788-- [ ] Comments on brews (atproto replies)
789789-- [ ] Share brews
790790-- [ ] Brew collections/lists
791791-792792-**Technical:**
793793-794794-- [ ] New lexicons for likes, follows, etc.
795795-- [ ] Integration with atproto social graph
796796-- [ ] Notification system
797797-798798-### Phase 9: Advanced Features (Future)
799799-800800-**Ideas for future consideration:**
801801-802802-- [ ] Statistics and analytics (personal insights)
803803-- [ ] Brew recipes and recommendations
804804-- [ ] Photo uploads (blob storage in PDS)
805805-- [ ] Equipment database (community-maintained)
806806-- [ ] Taste profile analysis
807807-- [ ] CSV import/export
808808-- [ ] Mobile native apps (using same lexicons)
809809-810810-### Phase 10: Performance & Scale (Future)
811811-812812-**Optimizations when needed:**
813813-814814-- [ ] Implement caching layer (Redis/SQLite)
815815-- [ ] Batch reference resolution
816816-- [ ] CDN for static assets
817817-- [ ] Optimize PDS queries
818818-- [ ] Background sync for index
819819-- [ ] Horizontal scaling
820820-821821----
822822-823823-## Open Questions & Decisions
824824-825825-### To Be Decided During Implementation
826826-827827-#### Authentication & Session Management
828828-829829-- **Q:** Use in-memory sessions (dev) or add Redis immediately?
830830-- **Q:** Session timeout duration?
831831-- **Q:** Support "remember me" functionality?
832832-- **Decision:** TBD based on hosting environment
833833-834834-#### Reference Resolution Strategy
835835-836836-- **Q:** Lazy loading (simple) or batch resolution (complex)?
837837-- **Q:** Cache resolved references?
838838-- **Decision:** Start with lazy loading, optimize later if needed
839839-840840-#### PDS Support
841841-842842-- **Q:** Bluesky only, or support any PDS from day 1?
843843-- **Q:** How to handle PDS discovery (handle → PDS URL)?
844844-- **Decision:** Support any PDS, use handle resolution
845845-846846-#### Error Handling Philosophy
847847-848848-- **Q:** Detailed errors for debugging vs user-friendly messages?
849849-- **Q:** Retry strategy for PDS operations?
850850-- **Decision:** User-friendly errors, log details, retry with backoff
851851-852852-#### Lexicon Publishing
853853-854854-- **Q:** Where to host lexicon files publicly?
855855-- **Options:**
856856- - GitHub repo (easy)
857857- - `.well-known` on domain (proper)
858858- - Both
859859-- **Decision:** GitHub for now, add .well-known later
860860-861861-#### Export Functionality
862862-863863-- **Q:** Keep JSON export feature?
864864-- **Q:** Export from PDS or from AppView cache?
865865-- **Decision:** Keep export, fetch from PDS
866866-867867-#### PWA/Offline
868868-869869-- **Q:** Remove service worker entirely?
870870-- **Q:** Keep PWA manifest for "Add to Home Screen"?
871871-- **Decision:** Keep manifest, update service worker for static-only caching
872872-873873----
874874-875875-## Timeline Summary
876876-877877-| Phase | Duration | Key Milestone |
878878-| --------------------------- | -------- | ------------------------------------------- |
879879-| 0: Research & Validation | 2-3 days | Lexicons validated, indigo SDK understood |
880880-| 1: Core atproto Client | 3-4 days | PDS operations working, SQLite removed |
881881-| 2: Authentication & OAuth | 3-4 days | Users can login with atproto OAuth (scopes) |
882882-| 3: Handler & UI Refactoring | 2-3 days | Full app working with atproto |
883883-| 4: Polish & Documentation | 2-3 days | Production ready, documented |
884884-| 5: Launch | Variable | Live with initial users |
885885-886886-**Total Estimated Time: 12-17 days** of focused development work
887887-888888-**Future phases:** TBD based on user feedback and priorities
889889-890890----
891891-892892-## Success Criteria
893893-894894-### Phase 1-4 Complete (Personal Tracker v1)
895895-896896-- [ ] Users can authenticate via atproto OAuth with proper scopes
897897-- [ ] Users can create, edit, delete all coffee tracking entities
898898-- [ ] Data persists in user's PDS (any PDS)
899899-- [ ] References between records work correctly
900900-- [ ] App is self-hostable by others
901901-- [ ] Documentation is complete and accurate
902902-- [ ] No dependency on SQLite for data storage
903903-- [ ] Existing UX/UI is preserved
904904-905905-### Future Success (Discovery & Social)
906906-907907-- [ ] Users can discover other coffee enthusiasts
908908-- [ ] Cross-user search and browsing works
909909-- [ ] Social features enable community building
910910-- [ ] AppView scales to many users
911911-912912----
913913-914914-## Resources & References
915915-916916-### Documentation
917917-918918-- [AT Protocol Docs](https://atproto.com)
919919-- [Indigo SDK](https://github.com/bluesky-social/indigo)
920920-- [Lexicon Specification](https://atproto.com/specs/lexicon)
921921-- [OAuth DPOP](https://atproto.com/specs/oauth)
922922-923923-### Example Applications
924924-925925-- Bluesky (reference implementation)
926926-- Other atproto apps (TBD - research during Phase 0)
927927-928928-### Tools
929929-930930-- [atproto CLI tools](https://github.com/bluesky-social/atproto)
931931-- PDS explorer tools
932932-- Lexicon validators
933933-934934----
935935-936936-## Notes
937937-938938-- This plan is a living document and will be updated as we learn more
939939-- Technical decisions may change based on discoveries during implementation
940940-- Timeline estimates are rough and may vary
941941-- Focus is on shipping a working v1 (personal tracker) before adding social features
942942-- OAuth must use scopes, not app passwords, for proper security and user control
4848+### Infrastructure
4949+- Production deployment guide
5050+- Monitoring and logging improvements
5151+- Rate limiting and abuse prevention
+45-163
README.md
···11-# Arabica - Coffee Brew Tracker
22-33-A self-hosted web application for tracking your coffee brewing journey. Built with Go and SQLite.
11+# Arabica
4255-## Features
66-77-- 📝 Quick entry of brew data (temperature, time, method, flexible grind size entry, etc.)
88-- ☕ Organize beans by origin and roaster with quick-select dropdowns
99-- 📱 Mobile-first PWA design for on-the-go tracking
1010-- 📊 Rating system and tasting notes
1111-- 📥 Export your data as JSON
1212-- 🔄 CRUD operations for all brew entries
1313-- 🗄️ SQLite database with abstraction layer for easy migration
33+Coffee brew tracking application using AT Protocol for decentralized storage.
144155## Tech Stack
1661717-- **Backend**: Go 1.22+ (using stdlib router)
1818-- **Database**: SQLite (via modernc.org/sqlite - pure Go, no CGO)
1919-- **Templates**: html/template (Go standard library)
2020-- **Frontend**: HTMX + Alpine.js
2121-- **CSS**: Tailwind CSS
2222-- **PWA**: Service Worker for offline support
2323-2424-## Project Structure
2525-2626-```
2727-arabica/
2828-├── cmd/server/ # Application entry point
2929-├── internal/
3030-│ ├── database/ # Database interface & SQLite implementation
3131-│ ├── models/ # Data models
3232-│ ├── handlers/ # HTTP handlers
3333-│ └── templates/ # HTML templates
3434-├── web/static/ # Static assets (CSS, JS, PWA files)
3535-└── migrations/ # Database migrations
3636-```
3737-3838-## Getting Started
3939-4040-### Prerequisites
4141-4242-Use Nix for a reproducible development environment with all dependencies:
77+- **Backend:** Go with stdlib HTTP router
88+- **Storage:** AT Protocol Personal Data Servers
99+- **Local DB:** BoltDB for OAuth sessions and feed registry
1010+- **Templates:** html/template
1111+- **Frontend:** HTMX + Alpine.js + Tailwind CSS
43124444-```bash
4545-nix develop
4646-```
1313+## Quick Start
47144848-### Running the Application
4949-5050-1. Enter the Nix development environment:
5115```bash
5252-nix develop
5353-```
1616+# Using Nix
1717+nix run
54185555-2. Build and run the server:
5656-```bash
5757-go run ./cmd/server
1919+# Or with Go
2020+go run cmd/server/main.go
5821```
59226060-The application will be available at `http://localhost:8080`
6161-6262-## Usage
6363-6464-### Adding a Brew
6565-6666-1. Navigate to "New Brew" from the home page
6767-2. Select a bean (or add a new one with the "+ New" button)
6868- - When adding a new bean, provide a **Name** (required) like "Morning Blend" or "House Espresso"
6969- - Optionally add Origin, Roast Level, and Description
7070-3. Select a roaster (or add a new one)
7171-4. Fill in brewing details:
7272- - Method (Pour Over, French Press, etc.)
7373- - Temperature (°C)
7474- - Brew time (seconds)
7575- - Grind size (free text - enter numbers like "18" or "3.5" for grinder settings, or descriptions like "Medium" or "Fine")
7676- - Grinder (optional)
7777- - Tasting notes
7878- - Rating (1-10)
7979-5. Click "Save Brew"
8080-8181-### Viewing Brews
8282-8383-Navigate to the "Brews" page to see all your entries in a table format with:
8484-- Date
8585-- Bean details
8686-- Roaster
8787-- Method and parameters
8888-- Rating
8989-- Actions (View, Delete)
9090-9191-### Exporting Data
9292-9393-Click "Export JSON" on the brews page to download all your data as JSON.
2323+Access at http://localhost:18910
94249525## Configuration
96269727Environment variables:
98289999-- `DB_PATH`: Path to SQLite database (default: `$HOME/.local/share/arabica/arabica.db` or XDG_DATA_HOME)
100100-- `PORT`: Server port (default: `18910`)
101101-- `LOG_LEVEL`: Logging level - `debug`, `info`, `warn`, or `error` (default: `info`)
102102-- `LOG_FORMAT`: Log output format - `console` (pretty, colored) or `json` (structured) (default: `console`)
103103-- `OAUTH_CLIENT_ID`: OAuth client ID for ATProto authentication (optional, uses localhost mode if not set)
104104-- `OAUTH_REDIRECT_URI`: OAuth redirect URI (optional, auto-configured for localhost)
105105-- `SECURE_COOKIES`: Set to `true` for production HTTPS environments (default: `false`)
106106-107107-### Logging
2929+- `PORT` - Server port (default: 18910)
3030+- `ARABICA_DB_PATH` - BoltDB path (default: ~/.local/share/arabica/arabica.db)
3131+- `OAUTH_CLIENT_ID` - OAuth client ID (optional, uses localhost mode if not set)
3232+- `OAUTH_REDIRECT_URI` - OAuth redirect URI (optional)
3333+- `SECURE_COOKIES` - Set to true for HTTPS (default: false)
3434+- `LOG_LEVEL` - Logging level: debug, info, warn, error (default: info)
3535+- `LOG_FORMAT` - Log format: console, json (default: console)
10836109109-The application uses [zerolog](https://github.com/rs/zerolog) for structured logging with the following features:
3737+## Features
11038111111-**Log Levels:**
112112-- `debug` - Detailed information including all PDS requests/responses
113113-- `info` - General application flow (default)
114114-- `warn` - Warning messages (non-fatal issues)
115115-- `error` - Error messages
3939+- Track coffee brews with detailed parameters
4040+- Store data in your AT Protocol Personal Data Server
4141+- Community feed of recent brews from registered users
4242+- Manage beans, roasters, grinders, and brewers
4343+- Export brew data as JSON
4444+- Mobile-friendly PWA design
11645117117-**Log Formats:**
118118-- `console` (default) - Human-readable, colored output for development
119119-- `json` - Structured JSON logs for production/log aggregation
4646+## Architecture
12047121121-**Request Logging:**
122122-All HTTP requests are logged with:
123123-- Method, path, query parameters
124124-- Status code, response time, bytes written
125125-- Client IP, user agent, referer
126126-- Authenticated user DID (if logged in)
127127-- Content type
4848+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.
12849129129-**PDS Request Logging:**
130130-All ATProto PDS operations are logged (at `debug` level) with:
131131-- Operation type (createRecord, getRecord, listRecords, etc.)
132132-- Collection name, record key
133133-- User DID
134134-- Request duration
135135-- Record counts for list operations
136136-- Pagination details
5050+Local BoltDB stores:
5151+- OAuth session data
5252+- Feed registry (list of DIDs for community feed)
13753138138-**Example configurations:**
5454+See docs/ for detailed documentation.
13955140140-Development (verbose):
141141-```bash
142142-LOG_LEVEL=debug LOG_FORMAT=console go run ./cmd/server
143143-```
5656+## Development
14457145145-Production (structured):
14658```bash
147147-LOG_LEVEL=info LOG_FORMAT=json SECURE_COOKIES=true ./arabica-server
148148-```
149149-150150-## Database Abstraction
151151-152152-The application uses an interface-based approach for database operations, making it easy to swap SQLite for PostgreSQL or another database later. See `internal/database/store.go` for the interface definition.
153153-154154-## PWA Support
155155-156156-The application includes:
157157-- Web App Manifest for "Add to Home Screen"
158158-- Service Worker for offline caching
159159-- Mobile-optimized UI with large touch targets
160160-161161-## Future Enhancements (Not in MVP)
162162-163163-- Statistics and analytics page
164164-- CSV export
165165-- Multi-user support (database already has user_id column)
166166-- Search and filtering
167167-- Photo uploads for beans/brews
168168-- Brew recipes and sharing
169169-170170-## Development Notes
5959+# Enter development environment
6060+nix develop
17161172172-### Why These Choices?
6262+# Run server
6363+go run cmd/server/main.go
17364174174-- **Go**: Fast compilation, single binary deployment, excellent stdlib
175175-- **modernc.org/sqlite**: Pure Go SQLite (no CGO), easy cross-compilation
176176-- **html/template**: Built-in Go templates, no external dependencies
177177-- **HTMX**: Progressive enhancement without heavy JS framework
178178-- **Nix**: Reproducible development environment
6565+# Run tests
6666+go test ./...
17967180180-### Database Schema
6868+# Build
6969+go build -o arabica cmd/server/main.go
7070+```
18171182182-See `migrations/001_initial.sql` for the complete schema.
7272+## Deployment
18373184184-Key tables:
185185-- `users`: Future multi-user support
186186-- `beans`: Coffee bean information
187187-- `roasters`: Roaster information
188188-- `brews`: Individual brew records with all parameters
7474+See docs/nix-install.md for NixOS deployment instructions.
1897519076## License
1917719278MIT
193193-194194-## Contributing
195195-196196-This is a personal project, but suggestions and improvements are welcome!
+1-150
docs/auth-required-ui.md
···11-# Auth-Required UI Changes
22-33-## Changes Made
44-55-### 1. Home Page - Login Button
66-77-**File:** `internal/templates/home.tmpl`
88-99-**When Not Authenticated:**
1010-- Shows welcome message explaining AT Protocol
1111-- Large "Log In with AT Protocol" button
1212-- Lists Arabica features (decentralized, portable data, etc.)
1313-1414-**When Authenticated:**
1515-- Shows user's DID
1616-- "Add New Brew" and "View All Brews" buttons
1717-- Full functionality
1818-1919-### 2. Navigation Bar - Conditional Links
2020-2121-**File:** `internal/templates/layout.tmpl`
2222-2323-**When Not Authenticated:**
2424-- Shows: Home, Login button
2525-- Hides: Brews, New Brew, Manage
2626-2727-**When Authenticated:**
2828-- Shows: Home, Brews, New Brew, Manage, Logout button
2929-- Logout button triggers POST to `/logout`
3030-3131-### 3. Removed SQLite Fallback
3232-3333-**File:** `internal/handlers/handlers.go`
3434-3535-**All protected handlers now:**
3636-```go
3737-// Require authentication
3838-store, authenticated := h.getAtprotoStore(r)
3939-if !authenticated {
4040- http.Redirect(w, r, "/login", http.StatusFound)
4141- return
4242-}
4343-```
4444-4545-**Affected handlers:**
4646-- `HandleBrewList` - redirects to login
4747-- `HandleBrewNew` - redirects to login
4848-- `HandleBrewEdit` - redirects to login
4949-- `HandleBrewCreate` - redirects to login
5050-- `HandleManage` - redirects to login
5151-- `HandleBeanCreate` - returns 401 (API endpoint)
5252-- `HandleRoasterCreate` - returns 401 (API endpoint)
5353-5454-**SQLite is NO LONGER used** - all data operations require authentication and use PDS storage.
5555-5656-### 4. Template Data Structure
5757-5858-**File:** `internal/templates/render.go`
5959-6060-**Added to PageData:**
6161-```go
6262-type PageData struct {
6363- // ... existing fields
6464- IsAuthenticated bool
6565- UserDID string
6666-}
6767-```
6868-6969-**All render functions updated** to accept and pass authentication status:
7070-- `RenderHome(w, isAuthenticated, userDID)`
7171-- `RenderBrewList(w, brews, isAuthenticated, userDID)`
7272-- `RenderBrewForm(w, beans, roasters, grinders, brewers, brew, isAuthenticated, userDID)`
7373-- `RenderManage(w, beans, roasters, grinders, brewers, isAuthenticated, userDID)`
7474-7575-## User Experience
7676-7777-### First Visit (Not Logged In)
7878-1. Visit http://localhost:18910
7979-2. See welcome page with "Log In with AT Protocol" button
8080-3. Nav bar only shows "Home" and "Login"
8181-4. Cannot access /brews, /manage, etc. (redirects to /login)
8282-8383-### After Login
8484-1. Click "Log In with AT Protocol"
8585-2. Enter your handle (e.g., `yourname.bsky.social`)
8686-3. Authenticate on your PDS
8787-4. Redirected to home page
8888-5. Nav bar shows all links + Logout button
8989-6. Full app functionality available
9090-7. All data stored in YOUR PDS
9191-9292-### Logout
9393-1. Click "Logout" in nav bar
9494-2. Session cleared
9595-3. Redirected to home page (unauthenticated view)
9696-9797-## Benefits
9898-9999-### Security
100100-- No data leakage between users
101101-- Cannot access app without authentication
102102-- Each user only sees their own data
103103-104104-### Privacy
105105-- Your data lives in YOUR PDS
106106-- Not stored on Arabica server
107107-- You control your data
108108-109109-### User Clarity
110110-- Clear distinction between logged in/out states
111111-- Obvious call-to-action to log in
112112-- Shows which DID you're logged in as
113113-114114-## Testing
115115-116116-1. **Start server:**
117117-```bash
118118-go build ./cmd/server
119119-./server
120120-```
121121-122122-2. **Visit home page (logged out):**
123123-- Go to http://localhost:18910
124124-- Should see login button
125125-- Nav bar should only show Home + Login
126126-127127-3. **Try accessing protected pages:**
128128-- Go to http://localhost:18910/brews
129129-- Should redirect to /login
130130-131131-4. **Log in:**
132132-- Click "Log In with AT Protocol"
133133-- Enter your Bluesky handle
134134-- Authenticate
135135-136136-5. **Verify logged in state:**
137137-- Home page shows your DID
138138-- Nav bar shows all links + Logout
139139-- Can access /brews, /manage, etc.
140140-141141-6. **Test logout:**
142142-- Click "Logout" in nav
143143-- Should return to unauthenticated home page
144144-145145-## Next Steps
146146-147147-Now that auth is working and UI is clean:
148148-1. Test creating a bean from /manage
149149-2. Verify it appears in your PDS
150150-3. Fix the ID/rkey issue so you can create brews that reference beans
11+# Deleted - Remove this file
+1-152
docs/future-witness-cache.md
···11-# Future Architecture Notes
22-33-## Witness Cache Pattern (from Paul Frazee)
44-55-### Concept
66-Many Atmosphere backends should start with a local "witness cache" of repositories.
77-88-**What is a Witness Cache?**
99-- A copy of repository records
1010-- Plus a timestamp of when the record was indexed (the "witness time")
1111-- Must be kept/preserved
1212-1313-### Key Benefits
1414-1515-#### 1. Local Replay Capability
1616-With local replay, you can:
1717-- Add new tables or indexes to your backend
1818-- Quickly backfill the data from local cache
1919-- **Without** having to backfill from the network (which is slow)
2020-2121-#### 2. Fast Iteration
2222-- Change your data model
2323-- Add new indexes
2424-- Reprocess data quickly
2525-- No network bottleneck
2626-2727-### Technology Recommendations
2828-2929-#### Good Candidates:
3030-3131-**RocksDB or other LSMs (Log-Structured Merge-Trees)**
3232-- Excellent write throughput
3333-- Good for high-volume ingestion
3434-- Used by many distributed systems
3535-3636-**ClickHouse**
3737-- Good compression ratio
3838-- Analytics-focused
3939-- Fast columnar queries
4040-4141-**DuckDB**
4242-- Good compression ratio
4343-- Embedded database
4444-- Great for analytics
4545-- Easy integration
4646-4747-### Implementation Timeline
4848-4949-**Phase 1-4 (Current):** Direct PDS queries (no cache)
5050-- Simple implementation
5151-- Works for single-user or small datasets
5252-- Good for understanding the data model
5353-5454-**Phase 6-7 (AppView + Indexing):** Add witness cache
5555-- When building firehose consumer
5656-- When indexing multiple users
5757-- When cross-user queries become important
5858-5959-**Phase 10 (Performance & Scale):** Optimize cache
6060-- Choose between RocksDB/ClickHouse/DuckDB based on usage patterns
6161-- Implement replay/backfill mechanisms
6262-- Add monitoring and metrics
6363-6464-### Architecture Sketch
6565-6666-```
6767-┌─────────────────────────────────────────────┐
6868-│ Firehose Consumer │
6969-│ (Subscribes to repo commit events) │
7070-└────────────────┬────────────────────────────┘
7171- │
7272- ↓ Write with witness time
7373-┌─────────────────────────────────────────────┐
7474-│ Witness Cache │
7575-│ (RocksDB / ClickHouse / DuckDB) │
7676-│ │
7777-│ Record: { │
7878-│ did: "did:plc:abc123" │
7979-│ collection: "com.arabica.brew" │
8080-│ rkey: "3jxy..." │
8181-│ record: {...} │
8282-│ witness_time: "2024-01-04T20:00:00Z" │
8383-│ cid: "baf..." │
8484-│ } │
8585-└────────────────┬────────────────────────────┘
8686- │
8787- ↓ Query/Transform
8888-┌─────────────────────────────────────────────┐
8989-│ Application Database │
9090-│ (PostgreSQL / SQLite) │
9191-│ │
9292-│ - Denormalized views │
9393-│ - Application-specific indexes │
9494-│ - Can be rebuilt from witness cache │
9595-└─────────────────────────────────────────────┘
9696-```
9797-9898-### Key Principles
9999-100100-1. **Witness cache is immutable append-only log**
101101- - Never delete records
102102- - Keep deletion markers
103103- - Keep all historical data
104104-105105-2. **Application DB is derived state**
106106- - Can be dropped and rebuilt
107107- - Optimized for queries
108108- - Contains denormalized data
109109-110110-3. **Replay = rebuild application DB from cache**
111111- - Fast because it's local
112112- - No network calls
113113- - Consistent state
114114-115115-### Migration Strategy
116116-117117-**Current (Phase 1-4):**
118118-```
119119-User's PDS → XRPC → Your App → SQLite (for UI)
120120-```
121121-122122-**Future (Phase 6+):**
123123-```
124124-Firehose → Witness Cache → Application DB → Your App
125125- ↓
126126-User's PDS (for writes)
127127-```
128128-129129-### Open Questions for Later
130130-131131-1. **Retention policy?**
132132- - Keep all historical data forever?
133133- - Compress old data?
134134- - Archive after N days?
135135-136136-2. **Consistency guarantees?**
137137- - Eventually consistent OK?
138138- - Need stronger guarantees?
139139-140140-3. **Witness time vs repo commit time?**
141141- - What if we receive events out of order?
142142- - How to handle backfills?
143143-144144-4. **Compression strategy?**
145145- - Compress old records?
146146- - Trade-off: space vs replay speed
147147-148148----
149149-150150-**Reference:** Paul Frazee's advice on Atmosphere backend architecture
151151-**Status:** Future enhancement (Phase 6+)
152152-**Priority:** Not needed for personal tracker v1
11+# Deleted - Remove this file
+1-193
docs/implementation-progress.md
···11-# Phase 0 Complete! ✅
22-33-## Summary
44-55-We've successfully completed Phase 0 (Research & Validation) and have begun Phase 1/2 implementation. The OAuth authentication system is now integrated and functional.
66-77-## What We Accomplished
88-99-### 1. Lexicon Validation ✅
1010-- Created 5 lexicon files for all Arabica record types
1111-- Validated all lexicons using `goat lex parse`
1212-- Passed lint checks with `goat lex lint`
1313-- Fixed issue with temperature field (changed from `number` to `integer` with tenths precision)
1414-1515-**Lexicon Files:**
1616-- `lexicons/com.arabica.bean.json`
1717-- `lexicons/com.arabica.roaster.json`
1818-- `lexicons/com.arabica.grinder.json`
1919-- `lexicons/com.arabica.brewer.json`
2020-- `lexicons/com.arabica.brew.json`
2121-2222-### 2. OAuth Integration ✅
2323-- Integrated indigo's OAuth client library
2424-- Created `internal/atproto/oauth.go` with OAuth manager
2525-- Implemented authentication handlers in `internal/handlers/auth.go`
2626-- Updated `cmd/server/main.go` to initialize OAuth
2727-2828-**OAuth Endpoints Implemented:**
2929-- `GET /login` - Login page
3030-- `POST /auth/login` - Initiate OAuth flow
3131-- `GET /oauth/callback` - Handle OAuth callback
3232-- `POST /logout` - Logout
3333-- `GET /client-metadata.json` - OAuth client metadata
3434-- `GET /.well-known/oauth-client-metadata` - Standard metadata endpoint
3535-3636-**Features:**
3737-- DPOP-bound access tokens (secure)
3838-- PKCE for public clients (no client secret needed)
3939-- Session management with cookies
4040-- Auth middleware for all requests
4141-- Scopes: `["atproto", "transition:generic"]`
4242-4343-### 3. Project Structure
4444-4545-```
4646-arabica-site/
4747-├── cmd/server/
4848-│ └── main.go # UPDATED: OAuth initialization
4949-├── internal/
5050-│ ├── atproto/ # NEW: atproto package
5151-│ │ └── oauth.go # OAuth manager
5252-│ ├── handlers/
5353-│ │ ├── auth.go # NEW: Auth handlers
5454-│ │ └── handlers.go # UPDATED: Added OAuth field
5555-│ ├── database/
5656-│ ├── models/
5757-│ └── templates/
5858-├── lexicons/ # NEW: Lexicon schemas
5959-│ ├── com.arabica.bean.json
6060-│ ├── com.arabica.roaster.json
6161-│ ├── com.arabica.grinder.json
6262-│ ├── com.arabica.brewer.json
6363-│ └── com.arabica.brew.json
6464-├── docs/
6565-│ ├── indigo-research.md
6666-│ ├── schema-design.md
6767-│ └── phase0-summary.md
6868-└── PLAN.md
6969-```
7070-7171-### 4. Build Status ✅
7272-- **Build:** Success ✅
7373-- **OAuth metadata endpoint:** Working ✅
7474-- **Login page:** Rendering ✅
7575-- **Ready for testing:** YES ✅
7676-7777-## Testing
7878-7979-The server successfully:
8080-1. Builds without errors
8181-2. Starts and listens on port 18910
8282-3. Serves OAuth client metadata correctly
8383-4. Displays login page
8484-8585-**OAuth Metadata Response:**
8686-```json
8787-{
8888- "client_id": "http://localhost:18910/client-metadata.json",
8989- "application_type": "web",
9090- "grant_types": ["authorization_code", "refresh_token"],
9191- "scope": "atproto transition:generic",
9292- "response_types": ["code"],
9393- "redirect_uris": ["http://localhost:18910/oauth/callback"],
9494- "token_endpoint_auth_method": "none",
9595- "dpop_bound_access_tokens": true
9696-}
9797-```
9898-9999-## Next Steps
100100-101101-### Phase 1: Core atproto Client (In Progress)
102102-Now that OAuth is working, we need to:
103103-104104-1. **Create atproto Client Wrapper** (`internal/atproto/client.go`)
105105- - Wrap indigo's XRPC client
106106- - Methods for record CRUD operations
107107- - Use authenticated sessions for API calls
108108-109109-2. **Implement Record Type Conversions** (`internal/atproto/records.go`)
110110- - Convert Go models → atproto records
111111- - Convert atproto records → Go models
112112- - Handle AT-URI references
113113-114114-3. **Implement Store Interface** (`internal/atproto/store.go`)
115115- - Replace SQLite implementation
116116- - Use PDS for all data operations
117117- - Handle reference resolution
118118-119119-### Testing OAuth Flow
120120-To test the complete OAuth flow, you'll need:
121121-1. A Bluesky account (or local PDS)
122122-2. Try logging in at `http://localhost:18910/login`
123123-3. Enter your handle (e.g., `alice.bsky.social`)
124124-4. Authorize the app on your PDS
125125-5. Get redirected back with session cookies
126126-127127-## Configuration
128128-129129-The app now supports these environment variables:
130130-131131-```bash
132132-# OAuth Configuration (optional, defaults to localhost)
133133-OAUTH_CLIENT_ID=http://localhost:18910/client-metadata.json
134134-OAUTH_REDIRECT_URI=http://localhost:18910/oauth/callback
135135-136136-# Server Configuration
137137-PORT=18910 # Server port
138138-DB_PATH=./arabica.db # SQLite database path (still used for now)
139139-```
140140-141141-## Key Decisions Made
142142-143143-1. **Temperature Storage:** Changed from `number` to `integer` (tenths of degree)
144144- - Reason: Lexicons don't support floating point
145145- - Solution: Store 935 for 93.5°C
146146-147147-2. **Session Storage:** Using in-memory MemStore for development
148148- - Production will need Redis or database-backed storage
149149- - Easy to swap out later
150150-151151-3. **Public Client:** Using OAuth public client (no secret)
152152- - PKCE provides security
153153- - Simpler for self-hosted deployments
154154- - DPOP binds tokens to client
155155-156156-4. **Local Development:** Using localhost URLs for OAuth
157157- - Works for development without HTTPS
158158- - Will need real domain for production
159159-160160-## Files Created/Modified
161161-162162-### Created:
163163-- `internal/atproto/oauth.go`
164164-- `internal/handlers/auth.go`
165165-- `lexicons/*.json` (5 files)
166166-- `docs/indigo-research.md`
167167-- `docs/schema-design.md`
168168-- `docs/phase0-summary.md`
169169-170170-### Modified:
171171-- `cmd/server/main.go` - Added OAuth setup
172172-- `internal/handlers/handlers.go` - Added OAuth field
173173-- `go.mod` / `go.sum` - Added indigo dependency
174174-- `PLAN.md` - Detailed OAuth section
175175-176176-## Known Issues / TODOs
177177-178178-1. **TODO:** Replace in-memory session store with persistent storage (Redis/SQLite)
179179-2. **TODO:** Set `Secure: true` on cookies in production (requires HTTPS)
180180-3. **TODO:** Create proper login template (currently inline HTML)
181181-4. **TODO:** Add error handling UI (currently raw HTTP errors)
182182-5. **TODO:** Implement Phase 1 - atproto client for record operations
183183-184184-## Resources
185185-186186-- **indigo SDK:** https://github.com/bluesky-social/indigo
187187-- **OAuth Demo:** `indigo/atproto/auth/oauth/cmd/oauth-web-demo/main.go`
188188-- **ATProto Specs:** https://atproto.com
189189-- **Lexicons:** See `lexicons/` directory
190190-191191----
192192-193193-**Status:** Phase 0 Complete ✅ | OAuth Integration Complete ✅ | Ready for Phase 1 🚀
11+# Deleted - Remove this file
+21-444
docs/indigo-research.md
···11-# Indigo SDK Research
11+# AT Protocol Integration
2233## Overview
44-The `indigo` SDK from Bluesky provides a comprehensive Go implementation for atproto, including:
55-- Complete OAuth client with DPOP support
66-- XRPC client for API operations
77-- Repository operations (create/read/update/delete records)
88-- DID resolution and handle management
99-- Lexicon support
1041111-**Package:** `github.com/bluesky-social/indigo`
1212-**Version:** v0.0.0-20260103083015-78a1c1894f36 (pseudo-version, tracks main branch)
55+Arabica uses the Bluesky indigo SDK for AT Protocol integration.
1361414----
77+**Package:** `github.com/bluesky-social/indigo`
1581616-## Key Packages
99+## Key Components
17101818-### 1. `atproto/auth/oauth` - OAuth Client Implementation
1919-**Location:** `github.com/bluesky-social/indigo/atproto/auth/oauth`
1111+### OAuth Authentication
20122121-**Features:**
2222-- Complete OAuth 2.0 + DPOP implementation
2323-- PKCE support (Proof Key for Code Exchange)
2424-- Public and confidential client support
2525-- Token refresh handling
2626-- Session management interfaces
2727-- PAR (Pushed Authorization Request) support
1313+- Public OAuth client with PKCE
1414+- DPOP-bound access tokens
1515+- Scopes: `atproto`, `transition:generic`
1616+- Session persistence via BoltDB
28172929-**Main Components:**
1818+### Record Operations
30193131-#### `ClientApp` - High-Level OAuth Client
3232-```go
3333-type ClientApp struct {
3434- Config ClientConfig
3535- Store ClientAuthStore
3636-}
2020+Standard AT Protocol record CRUD operations:
2121+- `com.atproto.repo.createRecord`
2222+- `com.atproto.repo.getRecord`
2323+- `com.atproto.repo.listRecords`
2424+- `com.atproto.repo.putRecord`
2525+- `com.atproto.repo.deleteRecord`
37263838-func NewClientApp(config *ClientConfig, store ClientAuthStore) *ClientApp
3939-```
4040-- Manages OAuth flow for multiple users
4141-- Handles session persistence
4242-- Automatic token refresh
4343-- Thread-safe for concurrent use
2727+### Client Implementation
44284545-#### `ClientConfig` - OAuth Configuration
4646-```go
4747-type ClientConfig struct {
4848- ClientID string // URL to client metadata (e.g., "https://arabica.com/client-metadata.json")
4949- RedirectURI string // Callback URL
5050- Scopes []string // e.g., ["atproto", "transition:generic"]
5151- // ... other fields
5252-}
2929+See `internal/atproto/client.go` for the XRPC client wrapper.
53305454-func NewPublicConfig(clientID, redirectURI string, scopes []string) ClientConfig
5555-```
5656-5757-#### `ClientAuthStore` - Session Persistence Interface
5858-```go
5959-type ClientAuthStore interface {
6060- GetSession(ctx context.Context, sessionID string) (*ClientAuth, error)
6161- PutSession(ctx context.Context, session *ClientAuth) error
6262- DeleteSession(ctx context.Context, sessionID string) error
6363-}
6464-```
6565-6666-**Built-in Implementations:**
6767-- `MemStore` - In-memory storage (ephemeral, for development)
6868-- Custom implementations needed for production (database-backed)
6969-7070-#### `ClientAuth` - Session State
7171-```go
7272-type ClientAuth struct {
7373- SessionID string
7474- DID string
7575- AccessToken string
7676- RefreshToken string
7777- TokenType string
7878- ExpiresAt time.Time
7979- Scope string
8080- // DPOP key material
8181-}
8282-```
3131+## References
83328484-**Example OAuth Flow (from doc.go):**
8585-```go
8686-// 1. Create client app (once at startup)
8787-config := oauth.NewPublicConfig(
8888- "https://arabica.com/client-metadata.json",
8989- "https://arabica.com/oauth/callback",
9090- []string{"atproto", "transition:generic"},
9191-)
9292-oauthApp := oauth.NewClientApp(&config, oauth.NewMemStore())
9393-9494-// 2. Initiate login
9595-state, authURL, err := oauthApp.Authorize(ctx, handle, stateParam)
9696-// Redirect user to authURL
9797-9898-// 3. Handle callback
9999-auth, err := oauthApp.HandleCallback(ctx, state, code)
100100-// auth.SessionID, auth.DID now available
101101-102102-// 4. Make authenticated requests
103103-token, err := oauthApp.TokenForSession(ctx, auth.SessionID)
104104-```
105105-106106-**OAuth Demo App:**
107107-- Example implementation: `atproto/auth/oauth/cmd/oauth-web-demo/main.go`
108108-- Shows complete flow with web UI
109109-- Good reference for our implementation
110110-111111----
112112-113113-### 2. `atproto/atclient` - Repository Operations
114114-**Location:** `github.com/bluesky-social/indigo/atproto/atclient`
115115-116116-Provides high-level client for atproto operations:
117117-- Record CRUD operations
118118-- Repository listing
119119-- Blob uploads
120120-- Identity resolution
121121-122122-**Key Types:**
123123-124124-#### `Client` - XRPC Client
125125-```go
126126-type Client struct {
127127- // HTTP client with auth
128128-}
129129-130130-// Record operations
131131-func (c *Client) CreateRecord(ctx context.Context, input *CreateRecordInput) (*CreateRecordOutput, error)
132132-func (c *Client) GetRecord(ctx context.Context, input *GetRecordInput) (*GetRecordOutput, error)
133133-func (c *Client) ListRecords(ctx context.Context, input *ListRecordsInput) (*ListRecordsOutput, error)
134134-func (c *Client) PutRecord(ctx context.Context, input *PutRecordInput) (*PutRecordOutput, error)
135135-func (c *Client) DeleteRecord(ctx context.Context, input *DeleteRecordInput) error
136136-```
137137-138138-**Record Input/Output Types:**
139139-```go
140140-type CreateRecordInput struct {
141141- Repo string // DID
142142- Collection string // e.g., "com.arabica.brew"
143143- Record map[string]interface{} // Record data
144144- RKey *string // Optional, uses TID if not provided
145145-}
146146-147147-type CreateRecordOutput struct {
148148- URI string // AT-URI of created record
149149- CID string // Content ID
150150-}
151151-152152-type GetRecordInput struct {
153153- Repo string // DID
154154- Collection string // e.g., "com.arabica.brew"
155155- RKey string // Record key
156156-}
157157-158158-type GetRecordOutput struct {
159159- URI string
160160- CID string
161161- Value map[string]interface{} // Record data
162162-}
163163-164164-type ListRecordsInput struct {
165165- Repo string // DID
166166- Collection string // e.g., "com.arabica.brew"
167167- Limit *int64 // Optional
168168- Cursor *string // For pagination
169169-}
170170-171171-type ListRecordsOutput struct {
172172- Records []Record
173173- Cursor *string
174174-}
175175-```
176176-177177----
178178-179179-### 3. `atproto/syntax` - AT-URI Parsing
180180-**Location:** `github.com/bluesky-social/indigo/atproto/syntax`
181181-182182-Parse and construct AT-URIs:
183183-```go
184184-// Parse AT-URI: at://did:plc:abc123/com.arabica.brew/3jxy...
185185-uri, err := syntax.ParseATURI("at://...")
186186-fmt.Println(uri.Authority()) // DID
187187-fmt.Println(uri.Collection()) // Collection name
188188-fmt.Println(uri.RecordKey()) // RKey
189189-190190-// Construct AT-URI
191191-uri := syntax.ATURI{
192192- Authority: did,
193193- Collection: "com.arabica.brew",
194194- RecordKey: rkey,
195195-}
196196-```
197197-198198----
199199-200200-### 4. `atproto/identity` - DID and Handle Resolution
201201-**Location:** `github.com/bluesky-social/indigo/atproto/identity`
202202-203203-Resolve handles to DIDs and discover PDS endpoints:
204204-```go
205205-// Resolve handle to DID
206206-did, err := identity.ResolveHandle(ctx, "alice.bsky.social")
207207-208208-// Resolve DID to PDS URL
209209-pdsURL, err := identity.ResolveDIDToPDS(ctx, did)
210210-211211-// Resolve DID document
212212-didDoc, err := identity.ResolveDID(ctx, did)
213213-```
214214-215215----
216216-217217-## Implementation Plan for Arabica
218218-219219-### Phase 1: OAuth Integration
220220-221221-**Files to Create:**
222222-- `internal/atproto/oauth.go` - Wrapper around indigo's OAuth
223223-- `internal/atproto/session.go` - Session storage implementation
224224-- `internal/atproto/middleware.go` - Auth middleware
225225-226226-**Key Decisions:**
227227-228228-1. **Session Storage:**
229229- - Development: Use `oauth.MemStore` (built-in)
230230- - Production: Implement `ClientAuthStore` with SQLite or Redis
231231-232232-2. **Client Metadata:**
233233- - Serve at `/.well-known/oauth-client-metadata` or at client_id URL
234234- - Generate from `config.ClientMetadata()`
235235- - Must be HTTPS in production
236236-237237-3. **Scopes:**
238238- - Start with: `["atproto", "transition:generic"]`
239239- - `atproto` scope gives full repo access
240240- - `transition:generic` allows legacy operations
241241-242242-4. **Client Type:**
243243- - Use **public client** (no client secret)
244244- - Simpler for self-hosted deployments
245245- - PKCE provides security
246246-247247-**Implementation Steps:**
248248-249249-1. Create OAuth wrapper:
250250-```go
251251-// internal/atproto/oauth.go
252252-type OAuthManager struct {
253253- app *oauth.ClientApp
254254-}
255255-256256-func NewOAuthManager(clientID, redirectURI string) (*OAuthManager, error) {
257257- config := oauth.NewPublicConfig(
258258- clientID,
259259- redirectURI,
260260- []string{"atproto", "transition:generic"},
261261- )
262262-263263- // Use MemStore for development
264264- store := oauth.NewMemStore()
265265-266266- app := oauth.NewClientApp(&config, store)
267267- return &OAuthManager{app: app}, nil
268268-}
269269-270270-func (m *OAuthManager) InitiateLogin(ctx context.Context, handle string) (string, error) {
271271- // Generate state, get auth URL, redirect
272272-}
273273-274274-func (m *OAuthManager) HandleCallback(ctx context.Context, code, state string) (*oauth.ClientAuth, error) {
275275- // Exchange code for tokens
276276-}
277277-278278-func (m *OAuthManager) GetSession(ctx context.Context, sessionID string) (*oauth.ClientAuth, error) {
279279- // Retrieve session
280280-}
281281-```
282282-283283-2. Add HTTP handlers:
284284-```go
285285-// internal/handlers/auth.go
286286-func (h *Handler) HandleLogin(w http.ResponseWriter, r *http.Request) {
287287- handle := r.FormValue("handle")
288288- authURL, err := h.oauth.InitiateLogin(r.Context(), handle)
289289- http.Redirect(w, r, authURL, http.StatusFound)
290290-}
291291-292292-func (h *Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
293293- code := r.URL.Query().Get("code")
294294- state := r.URL.Query().Get("state")
295295-296296- auth, err := h.oauth.HandleCallback(r.Context(), code, state)
297297-298298- // Set session cookie
299299- http.SetCookie(w, &http.Cookie{
300300- Name: "session_id",
301301- Value: auth.SessionID,
302302- HttpOnly: true,
303303- Secure: true, // HTTPS only in production
304304- SameSite: http.SameSiteLaxMode,
305305- })
306306-307307- http.Redirect(w, r, "/", http.StatusFound)
308308-}
309309-```
310310-311311-3. Create auth middleware:
312312-```go
313313-// internal/atproto/middleware.go
314314-func (m *OAuthManager) AuthMiddleware(next http.Handler) http.Handler {
315315- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
316316- cookie, err := r.Cookie("session_id")
317317- if err != nil {
318318- // No session, continue without auth
319319- next.ServeHTTP(w, r)
320320- return
321321- }
322322-323323- auth, err := m.GetSession(r.Context(), cookie.Value)
324324- if err != nil {
325325- // Invalid session
326326- http.Redirect(w, r, "/login", http.StatusFound)
327327- return
328328- }
329329-330330- // Add DID to context
331331- ctx := context.WithValue(r.Context(), "userDID", auth.DID)
332332- next.ServeHTTP(w, r.WithContext(ctx))
333333- })
334334-}
335335-```
336336-337337----
338338-339339-### Phase 2: Repository Operations
340340-341341-**Files to Create:**
342342-- `internal/atproto/client.go` - XRPC client wrapper
343343-- `internal/atproto/records.go` - Record type conversions
344344-- `internal/atproto/store.go` - Store interface implementation
345345-346346-**Implementation:**
347347-348348-1. Create authenticated XRPC client:
349349-```go
350350-// internal/atproto/client.go
351351-type Client struct {
352352- oauth *OAuthManager
353353- xrpc *xrpc.Client
354354-}
355355-356356-func (c *Client) GetAuthenticatedClient(ctx context.Context, sessionID string) (*xrpc.Client, error) {
357357- auth, err := c.oauth.GetSession(ctx, sessionID)
358358- if err != nil {
359359- return nil, err
360360- }
361361-362362- // Create XRPC client with auth
363363- client := &xrpc.Client{
364364- Host: auth.Host, // PDS URL
365365- Auth: &xrpc.AuthInfo{
366366- AccessJwt: auth.AccessToken,
367367- RefreshJwt: auth.RefreshToken,
368368- Did: auth.DID,
369369- Handle: auth.Handle,
370370- },
371371- }
372372-373373- return client, nil
374374-}
375375-```
376376-377377-2. Record CRUD operations:
378378-```go
379379-// internal/atproto/store.go
380380-type AtprotoStore struct {
381381- client *Client
382382- did string
383383-}
384384-385385-func (s *AtprotoStore) CreateBrew(brew *models.CreateBrewRequest, userID int) (*models.Brew, error) {
386386- // Convert brew to record
387387- record := brewToRecord(brew)
388388-389389- // Create record in PDS
390390- output, err := s.client.CreateRecord(ctx, &atclient.CreateRecordInput{
391391- Repo: s.did,
392392- Collection: "com.arabica.brew",
393393- Record: record,
394394- })
395395-396396- // Convert back to Brew model
397397- return recordToBrew(output)
398398-}
399399-```
400400-401401----
402402-403403-## Questions Answered
404404-405405-### ✅ Does indigo provide complete OAuth client with DPOP support?
406406-**YES** - Full implementation in `atproto/auth/oauth` package with:
407407-- DPOP JWT signing and nonces
408408-- Automatic token refresh with DPOP
409409-- Public and confidential client support
410410-- PKCE built-in
411411-412412-### ✅ What's the API for repo operations?
413413-**Answer:** `atproto/atclient` provides:
414414-- `CreateRecord()`, `GetRecord()`, `ListRecords()`, `PutRecord()`, `DeleteRecord()`
415415-- Input/output structs with proper typing
416416-- Handles AT-URIs and CIDs automatically
417417-418418-### ✅ Are there session management helpers?
419419-**YES** - `ClientAuthStore` interface with `MemStore` implementation:
420420-- Define interface for session persistence
421421-- Built-in in-memory store for development
422422-- Can implement custom stores (DB, Redis, etc.)
423423-424424-### ✅ How are AT-URIs parsed and resolved?
425425-**Answer:** `atproto/syntax` package:
426426-- `ParseATURI()` and `ATURI` type
427427-- Extract DID, collection, rkey components
428428-- Construct new AT-URIs
429429-430430----
431431-432432-## Next Steps
433433-434434-1. ✅ **Understand indigo OAuth** - COMPLETE
435435-2. **Create lexicon files** - Define schemas
436436-3. **Build OAuth wrapper** - Use indigo's ClientApp
437437-4. **Implement Store interface** - Use atclient for PDS operations
438438-5. **Update handlers** - Add auth context
439439-440440----
441441-442442-## Useful Resources
443443-444444-- **Indigo GitHub:** https://github.com/bluesky-social/indigo
445445-- **OAuth Demo:** `atproto/auth/oauth/cmd/oauth-web-demo/main.go`
446446-- **API Docs:** GoDoc for each package
447447-- **atproto Specs:** https://atproto.com/specs/oauth
448448-449449----
450450-451451-## Notes
452452-453453-- Indigo is actively developed, uses pseudo-versions (no tags)
454454-- OAuth implementation is production-ready and used by Bluesky
455455-- Good test coverage and examples
456456-- Community support via Bluesky Discord
457457-- Well-architected with clear interfaces
3333+- indigo SDK: https://github.com/bluesky-social/indigo
3434+- AT Protocol docs: https://atproto.com
+33
docs/nix-install.md
···11+# NixOS Installation
22+33+## Using the Module
44+55+Add to your configuration.nix:
66+77+```nix
88+{
99+ imports = [ ./arabica-site/module.nix ];
1010+1111+ services.arabica = {
1212+ enable = true;
1313+ port = 18910;
1414+ dataDir = "/var/lib/arabica";
1515+ logLevel = "info";
1616+ secureCookies = false; # Set true if behind HTTPS proxy
1717+ };
1818+}
1919+```
2020+2121+## Manual Installation
2222+2323+Build and run directly:
2424+2525+```bash
2626+# Build
2727+nix-build -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'
2828+2929+# Run
3030+result/bin/arabica
3131+```
3232+3333+The data directory will be created at `~/.local/share/arabica/` by default.
+1-78
docs/oauth-token-fix.md
···11-# OAuth Token Fix - DPOP Authentication
22-33-## The Problem
44-55-Getting error: `XRPC ERROR 400: InvalidToken: Malformed token`
66-77-## Root Cause
88-99-We were manually creating an XRPC client and setting the auth like this:
1010-1111-```go
1212-client.Auth = &xrpc.AuthInfo{
1313- AccessJwt: sessData.AccessToken,
1414- RefreshJwt: sessData.RefreshToken,
1515- // ...
1616-}
1717-```
1818-1919-**This doesn't work** because indigo's OAuth uses **DPOP (Demonstrating Proof of Possession)** tokens, which require:
2020-1. Special cryptographic signing of each request
2121-2. Token nonces that rotate
2222-3. Proof-of-possession headers
2323-2424-You can't just pass the access token directly - it needs DPOP signatures!
2525-2626-## The Solution
2727-2828-Use indigo's built-in `ClientSession` which handles DPOP automatically:
2929-3030-```go
3131-// Resume the OAuth session
3232-session, err := c.oauth.app.ResumeSession(ctx, did, sessionID)
3333-3434-// Get the authenticated API client
3535-apiClient := session.APIClient()
3636-3737-// Now make requests - DPOP is handled automatically
3838-apiClient.Post(ctx, "com.atproto.repo.createRecord", body, &result)
3939-```
4040-4141-## What Changed
4242-4343-**File:** `internal/atproto/client.go`
4444-4545-**Before:**
4646-- Manually created `xrpc.Client`
4747-- Set `client.Auth` with raw tokens ❌
4848-- Called `client.Do()` directly
4949-5050-**After:**
5151-- Call `app.ResumeSession()` to get `ClientSession`
5252-- Call `session.APIClient()` to get authenticated client ✅
5353-- Use `apiClient.Post()` and `apiClient.Get()` methods
5454-- DPOP signing happens automatically
5555-5656-## How DPOP Works (Behind the Scenes)
5757-5858-1. Each request gets a unique DPOP proof JWT
5959-2. The proof is signed with a private key stored in the session
6060-3. The proof includes:
6161- - HTTP method
6262- - Request URL
6363- - Current timestamp
6464- - Nonce from server
6565-4. PDS validates the proof matches the access token
6666-6767-**Why this matters:** Even if someone intercepts your access token, they can't use it without the private key to sign DPOP proofs.
6868-6969-## Testing
7070-7171-Now you should be able to create beans/brews and see them in your PDS!
7272-7373-Try creating a bean from `/manage` - it should work now.
7474-7575-To verify it's actually in your PDS:
7676-```bash
7777-curl "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=yourhandle.bsky.social&collection=com.arabica.bean"
7878-```
11+# Deleted - Remove this file
+1-205
docs/phase0-summary.md
···11-# Phase 0 Summary - Research & Validation
22-33-**Status:** ✅ COMPLETE
44-**Date:** January 4, 2024
55-66----
77-88-## Completed Tasks
99-1010-### ✅ Task 0.1: Research `indigo` SDK
1111-1212-**Findings:**
1313-- Indigo provides **complete OAuth client** with DPOP support
1414-- Package: `github.com/bluesky-social/indigo/atproto/auth/oauth`
1515-- High-level `ClientApp` API handles entire OAuth flow
1616-- Built-in `MemStore` for session storage (dev)
1717-- Excellent reference implementation in `oauth-web-demo`
1818-1919-**Key Components Identified:**
2020-- `ClientApp` - Main OAuth client (handles login/callback/refresh)
2121-- `ClientConfig` - Configuration (clientID, redirectURI, scopes)
2222-- `ClientAuthStore` - Session persistence interface
2323-- `ClientAuth` - Session state (DID, tokens, DPOP keys)
2424-2525-**Documentation:** See `docs/indigo-research.md` for full details
2626-2727-### ✅ Task 0.2: Design & Validate Lexicons
2828-2929-**Created 5 lexicon files:**
3030-1. `lexicons/com.arabica.bean.json` - Coffee beans
3131-2. `lexicons/com.arabica.roaster.json` - Roasters
3232-3. `lexicons/com.arabica.grinder.json` - Grinders
3333-4. `lexicons/com.arabica.brewer.json` - Brewing devices
3434-5. `lexicons/com.arabica.brew.json` - Brewing sessions
3535-3636-**Key Design Decisions:**
3737-- **References:** AT-URIs for all relationships (e.g., `beanRef`, `roasterRef`)
3838-- **Pours:** Embedded in brew records (not separate collection)
3939-- **Enums:** Limited use (grinder types) - mostly free text
4040-- **Required Fields:** Minimal (name + createdAt for most)
4141-- **User ID:** Removed (implicit from DID/repo)
4242-4343-**Schema Documentation:** See `docs/schema-design.md` for full schema
4444-4545-### ✅ Task 0.3: Set up Development Environment
4646-4747-**Dependencies Added:**
4848-- `github.com/bluesky-social/indigo` v0.0.0-20260103083015-78a1c1894f36
4949-5050-**Project Structure:**
5151-```
5252-arabica-site/
5353-├── docs/
5454-│ ├── indigo-research.md
5555-│ └── schema-design.md
5656-├── lexicons/
5757-│ ├── com.arabica.bean.json
5858-│ ├── com.arabica.roaster.json
5959-│ ├── com.arabica.grinder.json
6060-│ ├── com.arabica.brewer.json
6161-│ └── com.arabica.brew.json
6262-└── PLAN.md
6363-```
6464-6565-**Next Step:** Set up test PDS access (Bluesky account or local PDS)
6666-6767-### ⏸️ Task 0.4: Manual Record Creation Test
6868-6969-**Status:** Pending
7070-**Blocker:** Need test environment (PDS access)
7171-7272-**Options:**
7373-1. **Use Bluesky account** - Easiest, can test immediately
7474-2. **Run local PDS** - More control, requires Docker setup
7575-3. **Use atproto sandbox** - If available
7676-7777-**Recommendation:** Use Bluesky account for initial testing
7878-7979----
8080-8181-## Key Insights
8282-8383-### OAuth Implementation
8484-- **Use indigo's OAuth** - Production-ready, handles DPOP automatically
8585-- **Public client** - No client secret needed (PKCE provides security)
8686-- **Scopes:** `["atproto", "transition:generic"]`
8787-- **Client metadata:** Must be served via HTTPS at client_id URL
8888-- **Session storage:** Start with MemStore, add persistent store later
8989-9090-### Schema Design
9191-- **Decentralized by design** - Each user has their own beans/roasters
9292-- **Duplicates OK** - Users don't share entities (matches PDS model)
9393-- **Simple references** - AT-URIs keep it standard
9494-- **Embedded pours** - Reduces complexity and queries
9595-9696-### Reference Resolution
9797-- Need to fetch referenced records (bean, grinder, etc.) when displaying brews
9898-- Start with lazy loading (simple)
9999-- Optimize with batch fetching later if needed
100100-101101----
102102-103103-## Next Steps (Phase 1)
104104-105105-### Immediate Actions
106106-1. **Set up test PDS access**
107107- - Create Bluesky account (or use existing)
108108- - Get DID and access credentials
109109- - Test manual record creation
110110-111111-2. **Validate lexicons**
112112- - Use atproto CLI or tools to validate JSON schemas
113113- - Create test records manually
114114- - Verify they appear in repo
115115-116116-3. **Study oauth-web-demo**
117117- - Review example OAuth implementation
118118- - Understand flow and integration points
119119- - Identify patterns to follow
120120-121121-### Phase 1 Preparation
122122-Once testing environment is ready:
123123-- Begin implementing atproto client wrapper
124124-- Create OAuth handler using indigo
125125-- Implement Store interface via PDS operations
126126-127127----
128128-129129-## Questions for User
130130-131131-1. **PDS Testing:**
132132- - Do you have a Bluesky account we can use for testing?
133133- - Or should we set up a local PDS?
134134-135135-2. **Domain Setup:**
136136- - What domain will you use for the app?
137137- - (Needed for OAuth client_id and redirect_uri)
138138-139139-3. **Development Timeline:**
140140- - Ready to proceed to Phase 1 (implementation)?
141141- - Or need more time for research/planning?
142142-143143----
144144-145145-## Resources Created
146146-147147-- ✅ `docs/indigo-research.md` - Complete indigo SDK documentation
148148-- ✅ `docs/schema-design.md` - Lexicon schema specifications
149149-- ✅ `lexicons/*.json` - 5 lexicon files ready for validation
150150-- ✅ `PLAN.md` - Updated with detailed Phase 2 OAuth info
151151-152152----
153153-154154-## Confidence Level
155155-156156-**Phase 0 Assessment:** High confidence ✅
157157-158158-- Indigo SDK is well-suited for our needs
159159-- OAuth implementation is straightforward
160160-- Lexicon schemas are complete and validated conceptually
161161-- Clear path forward to implementation
162162-163163-**Risks Identified:**
164164-- None major for Phase 1
165165-- Session storage will need production solution eventually
166166-- Reference resolution performance TBD (likely fine)
167167-168168-**Ready to Proceed:** YES - Phase 1 can begin once test environment is set up
169169-170170----
171171-172172-## Time Spent
173173-174174-**Phase 0:** ~2 hours
175175-- Research: 1 hour
176176-- Schema design: 0.5 hours
177177-- Documentation: 0.5 hours
178178-179179-**Estimate vs Actual:** On track (planned 2-3 days, much faster due to good SDK docs)
180180-181181----
182182-183183-## Updated Phase 0 Checklist
184184-185185-- [x] Review `indigo` documentation
186186-- [x] Study OAuth implementation in indigo
187187-- [x] Understand XRPC client usage
188188-- [x] Review record CRUD operations API
189189-- [x] Find example applications (oauth-web-demo)
190190-- [x] Document findings in `docs/indigo-research.md`
191191-- [x] Write lexicon JSON files (all 5)
192192-- [ ] Validate lexicons against atproto schema validator
193193-- [x] Document schema decisions in `docs/schema-design.md`
194194-- [x] Review field mappings from SQLite schema
195195-- [ ] Create/use Bluesky account for testing
196196-- [ ] Install atproto development tools
197197-- [ ] Set up test DID
198198-- [ ] Configure environment variables
199199-- [ ] Manually create test records
200200-- [ ] Test all record types
201201-- [ ] Test cross-references
202202-- [ ] Verify records in repo explorer
203203-204204-**Progress:** 12/19 tasks complete (63%)
205205-**Remaining:** Test environment setup + manual validation
11+# Deleted - Remove this file
+30-367
docs/schema-design.md
···11-# Arabica Lexicon Schema Design
22-33-## Overview
44-55-This document describes the lexicon schemas for Arabica coffee tracking records in the AT Protocol ecosystem.
66-77-**Namespace:** `com.arabica.*`
88-**Records:** Public by default (visible via repo exploration)
99-**Key Format:** TID (Timestamp Identifier) - automatic
1010-1111----
1212-1313-## Schema Decisions
1414-1515-### Reference Strategy
1616-**Decision:** Use AT-URIs for all references between records
1717-1818-**Rationale:**
1919-- Maintains decentralization (references work across any PDS)
2020-- Standard atproto pattern
2121-- Allows users to reference their own records
2222-- Each user maintains their own copy of beans/roasters (duplicates OK)
2323-2424-**Example Reference:**
2525-```
2626-beanRef: "at://did:plc:abc123xyz/com.arabica.bean/3jxyabcd123"
2727-```
11+# Lexicon Schemas
2822929-### Pours Handling
3030-**Decision:** Embed pours as an array within brew records
33+## Record Types
3143232-**Rationale:**
3333-- Pours are tightly coupled to brews (no independent existence)
3434-- Simpler than separate collection
3535-- Reduces number of PDS queries
3636-- Matches original SQLite schema pattern
55+Arabica defines 5 lexicon schemas:
3763838-**Alternative Considered:** Separate `com.arabica.pour` collection
3939-- Rejected due to added complexity and query overhead
4040-4141-### Enum vs Free Text
4242-**Decision:** Mix of both approaches
77+### social.arabica.alpha.bean
88+Coffee bean records with origin, roast level, process, and roaster reference.
4394444-**Enums Used:**
4545-- `grinder.grinderType`: `["hand", "electric", "electric_hand"]`
4646-- `grinder.burrType`: `["conical", "flat", ""]` (empty string for unknown)
1010+### social.arabica.alpha.roaster
1111+Coffee roaster records with name, location, and website.
47124848-**Free Text Used:**
4949-- `bean.roastLevel`: User may have custom roast descriptions
5050-- `bean.process`: Processing methods vary
5151-- `brew.method`: Brewing methods are diverse
5252-- `brew.grindSize`: Can be numeric (grinder setting) or descriptive
1313+### social.arabica.alpha.grinder
1414+Grinder records with type (hand/electric), burr type (conical/flat), and notes.
53155454-**Rationale:**
5555-- Enums for truly limited, well-defined sets
5656-- Free text for user creativity and flexibility
5757-- Matches current app behavior
1616+### social.arabica.alpha.brewer
1717+Brewing device records with name and description.
58185959-### Field Mappings from SQLite
1919+### social.arabica.alpha.brew
2020+Brew session records including:
2121+- Bean reference (AT-URI)
2222+- Brewing parameters (temperature, time, water, coffee amounts)
2323+- Grinder and brewer references (optional)
2424+- Grind size, method, tasting notes, rating
2525+- Pours array (embedded, not separate records)
60266161-#### Removed Fields
6262-- `user_id` - Implicit (records exist in user's DID repo)
6363-- `id` - Replaced by TID (rkey)
2727+## Design Decisions
64286565-#### Reference Changes
6666-```
6767-SQLite → Lexicon
6868------------------------------------------
6969-bean.roaster_id (int) → bean.roasterRef (AT-URI)
7070-brew.bean_id (int) → brew.beanRef (AT-URI)
7171-brew.grinder_id (int) → brew.grinderRef (AT-URI)
7272-brew.brewer_id (int) → brew.brewerRef (AT-URI)
7373-```
2929+### References
3030+All references use AT-URIs pointing to user's own records.
3131+Example: `at://did:plc:abc123/social.arabica.alpha.bean/3jxy123`
74327575-#### Pours Changes
7676-```
7777-SQLite → Lexicon
7878--------------------------------------------
7979-pours table (separate) → brew.pours[] (embedded)
8080-pour.brew_id (int) → (removed, part of brew)
8181-pour.pour_number (int) → (removed, array index)
8282-```
3333+### Temperature Storage
3434+Stored as integer in tenths of degrees Celsius.
3535+Example: 935 represents 93.5°C
83368484----
8585-8686-## Schema Definitions
8787-8888-### 1. `com.arabica.bean`
8989-Coffee bean variety tracked by the user.
9090-9191-**Required Fields:**
9292-- `name` (string, max 200 chars)
9393-- `createdAt` (datetime)
9494-9595-**Optional Fields:**
9696-- `origin` (string, max 200 chars) - Geographic origin
9797-- `roastLevel` (string, max 100 chars) - Roast level description
9898-- `process` (string, max 100 chars) - Processing method
9999-- `description` (string, max 1000 chars) - Additional notes
100100-- `roasterRef` (at-uri) - Reference to roaster record
101101-102102-**Example:**
103103-```json
104104-{
105105- "name": "Ethiopian Yirgacheffe",
106106- "origin": "Ethiopia",
107107- "roastLevel": "Light",
108108- "process": "Washed",
109109- "description": "Floral and citrus notes",
110110- "roasterRef": "at://did:plc:abc/com.arabica.roaster/3jxy...",
111111- "createdAt": "2024-01-04T12:00:00Z"
112112-}
113113-```
114114-115115----
116116-117117-### 2. `com.arabica.roaster`
118118-Coffee roaster company.
119119-120120-**Required Fields:**
121121-- `name` (string, max 200 chars)
122122-- `createdAt` (datetime)
123123-124124-**Optional Fields:**
125125-- `location` (string, max 200 chars) - Location description
126126-- `website` (uri, max 500 chars) - Roaster website URL
127127-128128-**Example:**
129129-```json
130130-{
131131- "name": "Blue Bottle Coffee",
132132- "location": "Oakland, CA",
133133- "website": "https://bluebottlecoffee.com",
134134- "createdAt": "2024-01-04T12:00:00Z"
135135-}
136136-```
137137-138138----
139139-140140-### 3. `com.arabica.grinder`
141141-Coffee grinder equipment.
142142-143143-**Required Fields:**
144144-- `name` (string, max 200 chars)
145145-- `createdAt` (datetime)
146146-147147-**Optional Fields:**
148148-- `grinderType` (enum: `["hand", "electric", "electric_hand"]`)
149149-- `burrType` (enum: `["conical", "flat", ""]`) - Empty string for unknown
150150-- `notes` (string, max 1000 chars) - Additional notes
151151-152152-**Example:**
153153-```json
154154-{
155155- "name": "Baratza Encore",
156156- "grinderType": "electric",
157157- "burrType": "conical",
158158- "notes": "Great entry-level grinder",
159159- "createdAt": "2024-01-04T12:00:00Z"
160160-}
161161-```
162162-163163----
164164-165165-### 4. `com.arabica.brewer`
166166-Coffee brewing device or method.
167167-168168-**Required Fields:**
169169-- `name` (string, max 200 chars)
170170-- `createdAt` (datetime)
171171-172172-**Optional Fields:**
173173-- `description` (string, max 1000 chars) - Description or notes
174174-175175-**Example:**
176176-```json
177177-{
178178- "name": "Hario V60",
179179- "description": "Size 02, ceramic",
180180- "createdAt": "2024-01-04T12:00:00Z"
181181-}
182182-```
183183-184184----
185185-186186-### 5. `com.arabica.brew`
187187-Coffee brewing session with parameters.
188188-189189-**Required Fields:**
190190-- `beanRef` (at-uri) - Reference to bean used
191191-- `createdAt` (datetime)
192192-193193-**Optional Fields:**
194194-- `method` (string, max 100 chars) - Brewing method
195195-- `temperature` (number, 0-100) - Water temperature in Celsius
196196-- `waterAmount` (integer, ≥0) - Water amount in grams/ml
197197-- `timeSeconds` (integer, ≥0) - Total brew time
198198-- `grindSize` (string, max 50 chars) - Grind setting
199199-- `grinderRef` (at-uri) - Reference to grinder
200200-- `brewerRef` (at-uri) - Reference to brewer
201201-- `tastingNotes` (string, max 2000 chars) - Tasting notes
202202-- `rating` (integer, 1-10) - Rating
203203-- `pours` (array of pour objects) - Pour schedule
204204-205205-**Pour Object:**
206206-- `waterAmount` (integer, required) - Water in this pour
207207-- `timeSeconds` (integer, required) - Time of pour
208208-209209-**Example:**
210210-```json
211211-{
212212- "beanRef": "at://did:plc:abc/com.arabica.bean/3jxy...",
213213- "method": "Pour Over",
214214- "temperature": 93,
215215- "waterAmount": 300,
216216- "timeSeconds": 180,
217217- "grindSize": "18",
218218- "grinderRef": "at://did:plc:abc/com.arabica.grinder/3jxy...",
219219- "brewerRef": "at://did:plc:abc/com.arabica.brewer/3jxy...",
220220- "tastingNotes": "Bright acidity, notes of lemon and bergamot",
221221- "rating": 9,
222222- "pours": [
223223- {"waterAmount": 50, "timeSeconds": 0},
224224- {"waterAmount": 100, "timeSeconds": 45},
225225- {"waterAmount": 150, "timeSeconds": 90}
226226- ],
227227- "createdAt": "2024-01-04T08:30:00Z"
228228-}
229229-```
230230-231231----
232232-233233-## Data Relationships
234234-235235-```
236236-Roaster ←── Bean ←── Brew
237237- ↓
238238- Grinder
239239- ↓
240240- Brewer
241241- ↓
242242- Pours (embedded)
243243-```
244244-245245-**Key Points:**
246246-- All relationships are AT-URI references
247247-- References point within user's own repo (typically)
248248-- Users maintain their own copies of beans/roasters
249249-- Broken references should be handled gracefully in UI
250250-251251----
252252-253253-## Validation Rules
254254-255255-### String Lengths
256256-- Names/titles: 200 chars max
257257-- Short fields: 50-100 chars max
258258-- Descriptions/notes: 1000 chars max
259259-- Tasting notes: 2000 chars max
260260-- URLs: 500 chars max
261261-262262-### Numeric Constraints
263263-- Temperature: 0-100°C (reasonable coffee range)
264264-- Rating: 1-10 (explicit range)
265265-- Water amount, time: ≥0 (non-negative)
3737+### Pours
3838+Embedded in brew records as an array rather than separate collection.
2663926740### Required Fields
268268-- All records: `createdAt` (for temporal ordering)
269269-- Bean: `name` (minimum identifier)
270270-- Roaster: `name`
271271-- Grinder: `name`
272272-- Brewer: `name`
273273-- Brew: `beanRef` (essential relationship)
274274-275275----
276276-277277-## Schema Evolution
278278-279279-### Backward Compatibility
280280-If schema changes are needed:
281281-- Add new optional fields freely
282282-- Never remove required fields
283283-- Deprecate fields gradually
284284-- Use new collection IDs for breaking changes
285285-286286-### Versioning Strategy
287287-- Start with `com.arabica.*` (implicit v1)
288288-- If major breaking change needed: `com.arabica.v2.*`
289289-- Document migration paths
290290-291291----
292292-293293-## Publishing Lexicons
294294-295295-### Phase 1 (Current)
296296-- Lexicons in project repo: `lexicons/`
297297-- Not yet published publicly
298298-- For development use
299299-300300-### Phase 2 (Future)
301301-Host lexicons at one of:
302302-1. **GitHub Raw** (easiest):
303303- ```
304304- https://raw.githubusercontent.com/user/arabica/main/lexicons/com.arabica.brew.json
305305- ```
306306-307307-2. **Domain .well-known** (proper):
308308- ```
309309- https://arabica.com/.well-known/atproto/lexicons/com.arabica.brew.json
310310- ```
4141+Minimal requirements - most fields are optional for flexibility.
31142312312-3. **Both** (recommended):
313313- - GitHub for development/reference
314314- - Domain for production validation
4343+## Schema Files
31544316316----
317317-318318-## Testing Strategy
319319-320320-### Manual Testing (Phase 0)
321321-- [ ] Create test records with atproto CLI
322322-- [ ] Verify schema validation passes
323323-- [ ] Test all field types
324324-- [ ] Test reference resolution
325325-- [ ] Test with missing optional fields
326326-327327-### Automated Testing (Future)
328328-- Unit tests for record conversion
329329-- Integration tests for CRUD operations
330330-- Schema validation in CI/CD
331331-332332----
333333-334334-## Open Questions
335335-336336-1. **Should we version lexicons explicitly?**
337337- - Current: No version in ID
338338- - Alternative: `com.arabica.v1.brew`
339339-340340-2. **Should we add metadata fields?**
341341- - Examples: `version`, `updatedAt`, `tags`
342342- - Current: Minimal schema
343343-344344-3. **Should we support recipe sharing?**
345345- - Could add `recipeRef` field to brew
346346- - Future enhancement
347347-348348----
349349-350350-## Comparison to Original Schema
351351-352352-### SQLite Tables → Lexicons
353353-354354-| SQLite Table | Lexicon Collection | Changes |
355355-|--------------|-------------------|---------|
356356-| `users` | (removed) | Implicit - DID is the user |
357357-| `beans` | `com.arabica.bean` | `roaster_id` → `roasterRef` |
358358-| `roasters` | `com.arabica.roaster` | Minimal changes |
359359-| `grinders` | `com.arabica.grinder` | Enum types added |
360360-| `brewers` | `com.arabica.brewer` | Minimal changes |
361361-| `brews` | `com.arabica.brew` | Foreign keys → AT-URIs |
362362-| `pours` | (embedded in brew) | Now part of brew record |
363363-364364-### Key Architectural Changes
365365-- No user_id (implicit from repo)
366366-- No integer IDs (TID-based keys)
367367-- No foreign key constraints (AT-URI references)
368368-- No separate pours table (embedded)
369369-- Public by default (repo visibility)
370370-371371----
372372-373373-## Migration Notes
374374-375375-When migrating from SQLite to atproto:
376376-1. User ID 1 → Your DID
377377-2. Integer IDs → Create new TIDs
378378-3. Foreign keys → Look up AT-URIs
379379-4. Pours → Embed in brew records
380380-5. Timestamps → RFC3339 format
381381-382382-See `docs/migration-guide.md` (future) for detailed migration script.
4545+See `lexicons/` directory for complete JSON schemas.
+1-187
docs/testing-pds-storage.md
···11-# Testing PDS Storage
22-33-## Current Status
44-55-**YES, the app IS trying to write to your PDS!**
66-77-When you're logged in via OAuth, the handlers use `AtprotoStore` which makes XRPC calls to your Personal Data Server.
88-99-## How It Works
1010-1111-1. **Login** → OAuth flow → Session with your DID
1212-2. **Create Bean** → `POST /api/beans` → Calls `com.atproto.repo.createRecord` on your PDS
1313-3. **PDS stores the record** → Returns AT-URI like `at://did:plc:abc123/com.arabica.bean/3jxydef789`
1414-4. **App stores the record** → But has a problem with the returned ID...
1515-1616-## The ID Problem
1717-1818-**Current Issue:** The app expects integer IDs but PDS returns string rkeys (TIDs).
1919-2020-### What Happens Now
2121-2222-```
2323-User creates bean "Ethiopian Yirgacheffe"
2424- ↓
2525-App calls: CreateRecord("com.arabica.bean", {name: "Ethiopian Yirgacheffe", ...})
2626- ↓
2727-PDS returns: {uri: "at://did:plc:abc/com.arabica.bean/3jxy...", cid: "..."}
2828- ↓
2929-App extracts rkey: "3jxy..."
3030- ↓
3131-App sets bean.ID = 0 ← PROBLEM! Should store the rkey somewhere
3232- ↓
3333-Bean is returned to UI with ID=0
3434-```
3535-3636-### When Creating a Brew
3737-3838-```
3939-User selects bean from dropdown (shows ID=0 or wrong ID)
4040- ↓
4141-Form submits: {beanID: 0, ...}
4242- ↓
4343-App constructs: at://did:plc:abc/com.arabica.bean/0 ← INVALID!
4444- ↓
4545-PDS call fails: "Record not found"
4646-```
4747-4848-## Testing Right Now
4949-5050-### What Works ✅
5151-5252-1. **OAuth login** - You can log in with your atproto handle
5353-2. **List operations** - Will fetch from your PDS (empty initially)
5454-3. **Create operations** - Will call PDS API (but ID handling is broken)
5555-5656-### What Doesn't Work ❌
5757-5858-1. **Creating brews** - Requires valid bean references (IDs don't map correctly)
5959-2. **Editing records** - ID → rkey lookup fails
6060-3. **Deleting records** - ID → rkey lookup fails
6161-4. **References** - Can't construct valid AT-URIs from integer IDs
6262-6363-## How to Test Manually
6464-6565-### Test 1: Create a Bean (Direct API Call)
6666-6767-```bash
6868-# Log in first via browser at http://localhost:18910/login
6969-# Then get your session cookie and...
7070-7171-curl -X POST http://localhost:18910/api/beans \
7272- -H "Content-Type: application/json" \
7373- -H "Cookie: account_did=...; session_id=..." \
7474- -d '{
7575- "name": "Test Bean",
7676- "origin": "Ethiopia",
7777- "roast_level": "Light",
7878- "process": "Washed"
7979- }'
8080-```
8181-8282-**Watch the server logs** - you should see:
8383-- XRPC call to your PDS
8484-- Either success with returned URI, or an error
8585-8686-### Test 2: List Beans from PDS
8787-8888-```bash
8989-curl http://localhost:18910/manage \
9090- -H "Cookie: account_did=...; session_id=..."
9191-```
9292-9393-This will try to list all beans from your PDS.
9494-9595-### Test 3: Check Your PDS Directly
9696-9797-Use the atproto API to see what's actually in your repo:
9898-9999-```bash
100100-# List all records in com.arabica.bean collection
101101-curl "https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=yourhandle.bsky.social&collection=com.arabica.bean"
102102-```
103103-104104-## Debugging
105105-106106-### Enable Verbose Logging
107107-108108-The store already has debug printf statements:
109109-110110-```go
111111-fmt.Printf("Warning: failed to resolve brew references: %v\n", err)
112112-```
113113-114114-Watch your server console for these messages.
115115-116116-### Check XRPC Calls
117117-118118-The `client.go` makes XRPC calls via indigo's `client.Do()`. If there are errors, they'll be returned and logged.
119119-120120-## Next Steps to Fix
121121-122122-### Option 1: Store rkey in models (Quick Fix)
123123-124124-Add a `RKey string` field to all models:
125125-126126-```go
127127-type Bean struct {
128128- ID int `json:"id"`
129129- RKey string `json:"rkey"` // ← Add this
130130- Name string `json:"name"`
131131- // ...
132132-}
133133-```
134134-135135-Then update AtprotoStore to:
136136-1. Store the rkey when creating
137137-2. Use the rkey for updates/deletes
138138-3. Build AT-URIs from stored rkeys
139139-140140-### Option 2: In-Memory Mapping (Temporary)
141141-142142-Keep a map in AtprotoStore:
143143-144144-```go
145145-type AtprotoStore struct {
146146- // ...
147147- idToRKey map[string]map[int]string // collection -> id -> rkey
148148-}
149149-```
150150-151151-### Option 3: Use rkeys as IDs (Proper Fix)
152152-153153-Change models to use string IDs everywhere:
154154-155155-```go
156156-type Bean struct {
157157- ID string `json:"id"` // Now stores rkey like "3jxy..."
158158- Name string `json:"name"`
159159- // ...
160160-}
161161-```
162162-163163-This requires updating:
164164-- All handlers (parse string IDs, not ints)
165165-- Templates (use string IDs in forms)
166166-- Store interface (change signatures)
167167-168168-## Recommended Testing Path
169169-170170-1. **Update models to store rkeys** (Option 1)
171171-2. **Test bean creation** - verify record appears in PDS
172172-3. **Test bean listing** - verify records are fetched from PDS
173173-4. **Test brew creation with valid bean rkey**
174174-5. **Verify end-to-end flow works**
175175-176176-## Current Code Locations
177177-178178-- **Store implementation**: `internal/atproto/store.go`
179179-- **Record conversions**: `internal/atproto/records.go`
180180-- **XRPC client**: `internal/atproto/client.go`
181181-- **Handlers**: `internal/handlers/handlers.go`
182182-183183-## Summary
184184-185185-**You're 90% there!** The OAuth works, the XRPC calls are being made, the record conversions are correct. The only missing piece is proper ID/rkey handling so that references work correctly.
186186-187187-The quickest path forward is to add `RKey` fields to the models and update the store to use them.
11+# Deleted - Remove this file