Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

Teal Development Guidelines#

Build Commands#

  • Dev server: turbo dev --filter=@teal/aqua
  • Build all: pnpm build
  • Build Rust: pnpm build:rust
  • Test: pnpm test
  • DB migrate: pnpm db:migrate
  • DB reset: pnpm db:reset
  • Lexicon gen: pnpm lex:gen
  • Setup: ./scripts/setup-sqlx.sh

Project Structure#

teal/
├── apps/aqua/              # Main Rust/Axum web app
├── services/
│   ├── cadet/             # AT Protocol jetstream consumer
│   ├── rocketman/         # Jetstream library
│   ├── satellite/         # Processing service
│   ├── types/             # Shared types
│   └── migrations/        # SQLx migrations
├── lexicons/fm.teal.alpha/ # AT Protocol schemas
│   ├── feed/              # Music play types
│   ├── actor/             # Profile types
│   └── stats/             # Analytics
├── tools/lexicon-cli/      # Code generation
└── compose.yaml           # Docker setup

Tech Stack#

  • Backend: Rust (Axum, SQLx, Tokio, Serde, Tracing)
  • Database: PostgreSQL + Garnet/Redis
  • Frontend: TypeScript, Turbo, pnpm, Biome
  • Protocol: AT Protocol, IPLD/CAR, WebSocket streams
  • Deploy: Docker Compose, Traefik

Services#

  • Aqua: HTTP API, OAuth, play submission, profiles, CAR import
  • Cadet: Real-time jetstream consumer, background jobs, metrics
  • Rocketman: Reusable WebSocket consumer framework
  • Satellite: Analytics, aggregation, external APIs

Database Schema#

artists (mbid UUID, name TEXT, play_count INTEGER)
releases (mbid UUID, name TEXT, play_count INTEGER)
recordings (mbid UUID, name TEXT, play_count INTEGER)
plays (uri TEXT, did TEXT, rkey TEXT, cid TEXT, track_name TEXT,
       played_time TIMESTAMPTZ, recording_mbid UUID, release_mbid UUID,
       submission_client_agent TEXT, music_service_base_domain TEXT)
profiles (did TEXT, display_name TEXT, description TEXT, avatar BLOB, banner BLOB)

AT Protocol Integration#

Music Play Schema (fm.teal.alpha.feed.play)#

{
  "trackName": "string (required)",
  "trackMbId": "string", "recordingMbId": "string",
  "duration": "integer", "artists": [{"name": "string", "mbid": "string"}],
  "releaseName": "string", "releaseMbId": "string",
  "isrc": "string", "originUrl": "string",
  "musicServiceBaseDomain": "string", "submissionClientAgent": "string",
  "playedTime": "datetime"
}

Profile Schema (fm.teal.alpha.actor.profile)#

{
  "displayName": "string", "description": "string",
  "featuredItem": {"mbid": "string", "type": "album|track|artist"},
  "avatar": "blob", "banner": "blob", "createdAt": "datetime"
}

Core Features#

  • Music Tracking: primarily MusicBrainz metadata, multi-platform support, scrobbling logic
  • Social: Federated profiles, activity feeds, featured items, rich text
  • Real-time: WebSocket streams, CAR processing, background jobs
  • Analytics: Play counts, charts, materialized views

Development Setup#

# Install & setup
pnpm install
cp apps/aqua/.env.example apps/aqua/.env
./scripts/setup-sqlx.sh

# Start dependencies
docker compose -f compose.dev.yml up -d garnet postgres

# Run dev server
turbo dev --filter=@teal/aqua
# Access: http://localhost:3000

Database Operations#

pnpm db:create          # Create DB
pnpm db:migrate         # Run migrations
pnpm db:migrate:revert  # Revert migration
pnpm db:reset          # Reset DB
pnpm db:prepare        # Prepare queries

Lexicon Management#

pnpm lex:gen       # Generate types
pnpm lex:watch     # Watch & regenerate
pnpm lex:validate  # Validate schemas
pnpm lex:diff      # Show changes

Rust Development Guidelines#

  • Error Handling: Always use anyhow::Result<T> for fallible functions, never Result<T, String>
  • Async Patterns: Use async/await throughout, avoid tokio::spawn unless necessary for parallelism
  • Database: SQLx queries MUST be compile-time verified with cargo sqlx prepare
  • Types: Use workspace types from types crate, avoid duplicating structs across services
  • Imports: Group std → external → workspace → local, use use not extern crate
  • Lifetimes: Avoid explicit lifetimes unless required, let compiler infer
  • Memory: Use Arc<T> for shared ownership, Rc<T> only in single-threaded contexts
  • Serialization: Use #[derive(Serialize, Deserialize)] with serde, not manual impls
  • Logging: Use tracing::info!, tracing::error! etc, not println! or log crate
  • Testing: Place unit tests in #[cfg(test)] modules, integration tests in tests/ directory

Git Workflow Guidelines#

  • CLAUDE: You MUST work in a branch, you can NOT push to main and thus you need to make a PR per-feature
  • Branch naming: feature/short-description, fix/issue-description, refactor/component-name
  • Commit format: type(scope): description - e.g. feat(cadet): add jetstream reconnection, fix(aqua): handle empty play submissions
  • Commit types: feat, fix, refactor, test, docs, chore, perf, style
  • Branch lifecycle: Create from main, keep updated with git rebase main, squash before merge
  • PR requirements: All tests pass, no clippy warnings, SQLx queries prepared, lexicons validated
  • Commit size: Small, focused commits - one logical change per commit
  • Message body: Include WHY for non-obvious changes, reference issues with #123
  • Breaking changes: Mark with BREAKING: in commit body, update migration docs

Code Standards#

  • TypeScript: Biome format (pnpm fix), explicit types
  • DB: Snake_case, migrations, indexes, constraints
  • AT Protocol: URI format, lexicon validation, federation-ready

Testing#

pnpm test           # All tests
pnpm test:rust      # Rust only
cargo test          # Service tests
cargo tarpaulin     # Coverage

Deployment#

# Environment vars: DATABASE_URL, REDIS_URL, AT_PROTOCOL_JWT_SECRET
docker compose build
docker compose up -d
docker compose exec aqua-api pnpm db:migrate

Architecture#

  • Microservices: Rust services with shared types
  • Federation: AT Protocol for decentralized social music
  • Scrobble When: <2min = full play, ≥2min = half duration (max 4min)
  • Data Flow: WebSocket → Cadet → PostgreSQL → Aqua API
  • Caching: Redis for sessions, jobs, API responses
  • Monitoring: Prometheus metrics, structured logging

Key Patterns#

  • Lexicon-first: Schema definitions drive code generation
  • Event-driven: Real-time processing via jetstream
  • Content-addressable: CAR files for federated data
  • Type-safe: SQLx compile-time query verification
  • Async-first: Tokio runtime throughout

Rust Anti-Patterns to Avoid#

  • DON'T: Use unwrap() or expect() in production code - use proper error handling
  • DON'T: Clone unnecessarily - prefer borrowing with & or using Arc<T>
  • DON'T: Use String when &str suffices - avoid unnecessary allocations
  • DON'T: Mix blocking and async code - use tokio::task::spawn_blocking for CPU work
  • DON'T: Ignore compiler warnings - fix all clippy lints before committing
  • DON'T: Write raw SQL strings - use SQLx query macros or builder patterns
  • DON'T: Use global state - pass dependencies through function parameters or structs
  • DON'T: Implement Debug manually - use #[derive(Debug)] unless custom formatting needed