Teal Development Guidelines
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
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/ # AT Protocol schemas
│ ├── feed/ # Music play types
│ ├── actor/ # Profile types
│ └── stats/ # Analytics
├── tools/lexicon-cli/ # Code generation
└── compose.yaml # Docker setup
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
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
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)
Music Play Schema (fm.teal.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.actor.profile)
{
"displayName" : " string" , "description" : " string" ,
"featuredItem" : {"mbid" : " string" , "type" : " album|track|artist" },
"avatar" : " blob" , "banner" : " blob" , "createdAt" : " datetime"
}
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
# 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
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
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
Codex: 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
TypeScript : Biome format (pnpm fix), explicit types
DB : Snake_case, migrations, indexes, constraints
AT Protocol : URI format, lexicon validation, federation-ready
pnpm test # All tests
pnpm test:rust # Rust only
cargo test # Service tests
cargo tarpaulin # Coverage
# 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
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
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