CLAUDE.md#
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview#
Arabica is a coffee brew tracking application built on AT Protocol. User data (beans, brews, cafes, drinks, etc.) lives in each user's Personal Data Server (PDS), not locally. The app authenticates via OAuth, then performs CRUD through XRPC calls to the user's PDS.
Build & Development Commands#
# Run development server (debug logging, moderator config, known-dids backfill)
just run
# Run tests
go test ./...
# Run a single test
go test ./internal/models/... -run TestBeanIsIncomplete
# After editing .templ files — regenerate Go code
templ generate # all files
templ generate -f <file> # single file
# Rebuild Tailwind CSS (required after CSS/class changes)
just style
# Verify after changes
go vet ./...
go build ./...
Workflow Rules#
Do NOT spend more than 2-3 minutes exploring/reading files before beginning implementation. If the task is clear, start writing code immediately. Ask clarifying questions rather than endlessly reading the codebase. When given a specific implementation task, produce code changes in the same session.
Work Management#
This project uses cells for task tracking. See .cells/AGENTS.md for usage.
./cells list/./cells list --status open/./cells show <cell-id>- Do NOT use
./cells run(spawns new agent session, humans only)
Dependencies#
Prefer standard library solutions over external dependencies. Only add a third-party dependency if stdlib genuinely cannot handle the requirement.
Task Agents#
Hard limit of 3 agents maximum. Each agent must have a clearly scoped deliverable. Do not poll agents in a loop. If agents aren't producing results within 5 minutes, fall back to doing the work directly.
Tech Stack#
- Language: Go 1.21+, stdlib
net/httpwith Go 1.22 routing - Storage: AT Protocol PDS (user data), BoltDB (sessions), SQLite (firehose index)
- Frontend: HTMX + Alpine.js + Tailwind CSS
- Templates: Templ (type-safe Go templates)
- Logging: zerolog
Architecture#
AT Protocol Integration#
- User authenticates via OAuth (indigo SDK handles PKCE/DPOP)
- Handler creates
AtprotoStorescoped to user's DID + session - Store methods make XRPC calls to user's PDS
- Results rendered via Templ components or returned as JSON
Collections (NSIDs) — defined in internal/atproto/nsid.go:
social.arabica.alpha.bean— Coffee beans (references roaster)social.arabica.alpha.roaster— Roasterssocial.arabica.alpha.grinder— Grinderssocial.arabica.alpha.brewer— Brewing devicessocial.arabica.alpha.cafe— Cafes (references roaster)social.arabica.alpha.brew— Brew sessions (references bean, grinder, brewer, recipe)social.arabica.alpha.drink— Drinks at cafes (references cafe, bean)social.arabica.alpha.recipe— Recipes (references brewer)social.arabica.alpha.like— Likes (strongRef to any record)social.arabica.alpha.comment— Comments (strongRef to any record, optional parent for threads)
Records reference each other via AT-URIs (at://did/collection/rkey). Record
keys use TID format (timestamp-based identifiers).
Store Interface#
internal/database/store.go defines the Store interface with CRUD methods for
all entity types. AtprotoStore is the production implementation backed by the
user's PDS with witness cache and session cache layers.
Three-Layer Caching#
-
SessionCache (
internal/atproto/cache.go) — per-user in-memory cache (2-min TTL). Copy-on-write pattern, invalidated on writes. Dirty-collection tracking skips witness cache after local writes until firehose catches up. -
WitnessCache (
internal/firehose/index.go) — SQLite-backed local index populated by the Jetstream firehose consumer. Provides fast reads without PDS calls. Used as fallback when session cache misses. -
PDS fallback — direct XRPC calls to the user's PDS when both caches miss.
Write path: PDS write -> write-through to witness cache -> invalidate session cache (mark dirty).
Firehose & Feed Pipeline#
internal/firehose/ subscribes to AT Protocol's Jetstream relay for real-time
events. Records are indexed into the SQLite feed index. The feed pipeline:
- FeedIndex (
firehose/index.go) — SQLite store,recordToFeedItem()converts indexed records toFeedItemstructs with resolved references. - FeedIndexAdapter (
firehose/adapter.go) — converts firehoseFeedItemto feedFirehoseFeedItemto avoid import cycles. - Feed Service (
feed/service.go) — converts tofeed.FeedItem, applies moderation filtering, caching, and pagination.
When adding a new entity type, all three layers need the new fields added.
Adding a New Entity Type (Checklist)#
The full stack for a new entity requires changes across many files. Follow the pattern of an existing entity (e.g., roaster for simple entities, brew for entities with references):
- Lexicon JSON in
lexicons/ - NSID constant in
internal/atproto/nsid.go - RecordType constant in
internal/lexicons/record_type.go(const + ParseRecordType + DisplayName) - Model + request types + validation in
internal/models/models.go - Record conversion (
XToRecord/RecordToX) ininternal/atproto/records.go - Store interface methods in
internal/database/store.go - AtprotoStore implementation in
internal/atproto/store.go(CRUD + witness + cache) - Cache fields + Set/Invalidate methods in
internal/atproto/cache.go - OAuth scope in
internal/atproto/oauth.go - Firehose config (collection list) in
internal/firehose/config.go - Firehose FeedItem fields +
recordToFeedItemswitch case ininternal/firehose/index.go - Feed adapter mapping in
internal/firehose/adapter.go - Feed service
FeedItem+FirehoseFeedItemfields and both mapper sites ininternal/feed/service.go - CRUD handlers in
internal/handlers/entities.go(also updateHandleManagePartial,HandleAPIListAll,HandleManageRefresh) - View + OG image handlers in
internal/handlers/entity_views.go - Modal handlers in
internal/handlers/modals.go - Routes in
internal/routing/routing.go(page views, API CRUD, modals, OG images) - Templ view page in
internal/web/pages/(e.g.,cafe_view.templ) - Templ record content in
internal/web/components/(e.g.,record_cafe.templ) - Entity table component in
internal/web/components/entity_tables.templ - Dialog modal in
internal/web/components/dialog_modals.templ(+getStringValuecases) - Manage partial tab in
internal/web/components/manage_partial.templ - My Coffee tab in
internal/web/pages/my_coffee.templ - Feed card switch cases in
internal/web/pages/feed.templ(card class, content, ActionText, share URL, title, delete URL) - OG card function in
internal/ogcard/entities.go(+ accent color inbrew.go) - Suggestions config in
internal/suggestions/suggestions.go+ handler map ininternal/handlers/suggestions.go - Client-side cache entity case in
static/js/combo-select.jsgetUserEntities()
Templ Architecture#
Tabs only in .templ files — never use spaces for indentation. A post-edit
hook runs templ fmt automatically. After editing .templ files, run
templ generate to regenerate Go code.
Pages (internal/web/pages/) accept *components.LayoutData + page-specific
props. Components (internal/web/components/) are reusable building blocks.
Pattern: pages.PageName(layoutData, props).Render(r.Context(), w)
Combo-Select Component System#
Entity selection dropdowns (bean, grinder, brewer, roaster, cafe) use a shared combo-select pattern with typeahead search, community suggestions, and inline creation:
- Go config:
components.ComboSelectConfig()incomponents/combo_select.templgenerates Alpine.jsx-datawith entity-specific label formatting and create data mapping. - Templ markup:
components.ComboSelectInput()renders the shared dropdown UI. - JS behavior:
static/js/combo-select.js— Alpine.js component that searches user records (from client-side cache), community suggestions (from/api/suggestions/{entity}), and creates new entities inline via POST. - Suggestions backend:
internal/suggestions/suggestions.go— entity configs define searchable fields and dedup keys.
To add a new entity to combo-select: add a case to ComboSelectConfig, add to
getUserEntities() in combo-select.js, add entity config to
suggestions.go, and add to the entity-to-NSID map in
handlers/suggestions.go.
Entity View Handler Pattern#
View handlers (HandleXView) support both authenticated (own records) and
public (via ?owner= parameter) access. They:
- Try witness cache first, fall back to PDS
- Resolve references (e.g., roaster for cafe)
- Populate OG metadata for social sharing
- Fetch social data (likes, comments, moderation state)
- Render the templ page with all props
CSS Cache Busting#
When making CSS/style changes, bump the version query parameter in
internal/web/components/layout.templ:
<link rel="stylesheet" href="/static/css/output.css?v=0.1.3" />
Testing Conventions#
All tests MUST use testify/assert. Do
NOT use if statements with t.Error().
assert.Equal(t, expected, actual)
assert.NoError(t, err)
assert.Contains(t, haystack, needle)
assert.True(t, value)
assert.Nil(t, value)
Using Go Tooling#
go mod download -json MODULE— get dependency source pathgo doc foo.Bar— read package/type/function docsgo run ./cmd/serverinstead ofgo buildto avoid artifacts
Design Context#
See .impeccable.md for the full design system reference. Key points:
Brand Personality#
Cozy, social, inviting — like a neighborhood specialty cafe. Warm, not clinical. The emotional goals are calm satisfaction, geeky delight, community belonging, and craft pride.
Visual References#
- Specialty coffee bag packaging (Counter Culture, Onyx) — craft labels, earthy tones, confident type
- Analog journals — Moleskine, handwritten brew logs, texture of paper and ink
Design Principles#
- Warmth over precision — Brown paper, not graph paper
- Quiet confidence — Strong typography, restrained color, let content shine
- Tactile texture — Evoke the analog: ceramic, kraft, journal pages
- Community as atmosphere — Cafe conversations, not social media timelines
- Respect the ritual — No urgency, no gamification, intentional interactions
Typography#
Iosevka Patrick (custom monospace) is the core UI font. Open to pairing with a warmer display font for headings.