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.
# Run tests
go test ./...
# Regenerate templ after changes
templ generate
# Verify after changes
go vet ./...
go build ./...
# Format
just formatPrefer jj over git for all version control operations in this repo. Fall
back to git only when jj cannot accomplish the task.
Prefer standard library solutions over external dependencies. Only add a third-party dependency if stdlib genuinely cannot handle the requirement.
- Language: Go 1.26+, stdlib
net/httpwith Go 1.22 routing - Storage: atproto PDS (user data), SQLite (firehose index, auth sessions)
- Frontend: HTMX + Svelte islands + plain CSS (utility-class pattern) + Templ (templates)
- 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).
internal/arabica/store/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.
-
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).
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 tofeed.FeedItemstructs with resolved references. - FeedIndexAdapter (
firehose/adapter.go) — implementsfeed.FirehoseIndexoverFeedIndex. Items pass through as*feed.FeedItem; the adapter only bridges theFirehoseFeedQuery/FeedQueryquery structs. - Feed Service (
feed/service.go) — applies moderation filtering, caching, and pagination. The handler populatesIsLikedByViewerandIsOwneron each item once a viewer is identified.
When adding a new entity type, fields live on a single feed.FeedItem struct;
entity-specific population lives in recordToFeedItem's switch.
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/<namespace>/(path mirrors the NSID, e.g.lexicons/social/arabica/alpha/bean.json) - 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/arabica/store/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 recordToFeedItemswitch case ininternal/firehose/index.go(entity-specific population onfeed.FeedItem)FeedItemfields ininternal/feed/service.goif the new entity needs a field on the shared payload- 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
internal/web/assets/svelte/src/EntityCombo.sveltecachedEntities()
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)
Entity selection dropdowns (bean, grinder, brewer, roaster, cafe) use a shared combo-select pattern with typeahead search, community suggestions, and inline creation:
- Templ markup: complex forms render Svelte island mount points and use
EntityCombo.svelteinside the island for entity-specific selections. - Svelte behavior:
internal/web/assets/svelte/src/EntityCombo.sveltesearches user records from the Svelte app cache, community suggestions from/api/suggestions/{entity}, and creates new entities inline via POST. - Entity config:
internal/web/assets/svelte/src/comboSelectRegistry.tsowns entity-specific label formatting, extra fields, and create data mapping. - Suggestions backend:
internal/suggestions/suggestions.go— entity configs define searchable fields and dedup keys.
To add a new entity to combo-select: add entity config to
comboSelectRegistry.ts, add the cached entity case to cachedEntities() in
EntityCombo.svelte, add entity config to suggestions.go, and add to the
entity-to-NSID map in handlers/suggestions.go.
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
The internal/web/assets package owns the front-end source tree:
All files are go:embeded and served from in-memory caches:
- CSS: concatenated into one bundle per app at startup; URL is
/static/css/output.css(or/static/css/output-<app>.css) with?h=<sha256-prefix>cache buster auto-derived from content - JS: served per-file at
/static/js/<name>with the same?h=...query param; templates reference each file via{ assets.JSHrefFor("name.js") }
For dev, set ARABICA_DEV=1 for hot reloading of assets.
All tests MUST use testify:
assert.Equal(t, expected, actual)
assert.NoError(t, err)
assert.Contains(t, haystack, needle)
assert.True(t, value)Prefer table driven tests.
go mod download -json MODULE— get dependency source pathgo doc foo.Bar— read package/type/function docsgo run ./cmd/arabicainstead ofgo buildto avoid artifacts
See DESIGN.md and PRODUCT.md for the full design system reference.