QuickDID - Development Guide for Claude#
Overview#
QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides bidirectional handle-to-DID and DID-to-handle resolution with multi-layer caching (Redis, SQLite, in-memory), queue processing, metrics support, proactive cache refreshing, and real-time cache updates via Jetstream consumer.
Configuration#
QuickDID follows the 12-factor app methodology and uses environment variables exclusively for configuration. There are no command-line arguments except for --version and --help.
Configuration is validated at startup, and the service will exit with specific error codes if validation fails:
error-quickdid-config-1: Missing required environment variableerror-quickdid-config-2: Invalid configuration valueerror-quickdid-config-3: Invalid TTL value (must be positive)error-quickdid-config-4: Invalid timeout value (must be positive)
Common Commands#
Building and Running#
# Build the project
cargo build
# Run in debug mode (requires environment variables)
HTTP_EXTERNAL=localhost:3007 cargo run
# Run tests
cargo test
# Type checking
cargo check
# Linting
cargo clippy
# Show version
cargo run -- --version
# Show help
cargo run -- --help
Development with VS Code#
The project includes a .vscode/launch.json configuration for debugging with Redis integration. Use the "Debug executable 'quickdid'" launch configuration.
Architecture#
Core Components#
-
Handle Resolution (
src/handle_resolver/)BaseHandleResolver: Core resolution using DNS and HTTPRateLimitedHandleResolver: Semaphore-based rate limiting with optional timeoutCachingHandleResolver: In-memory caching layer with bidirectional supportRedisHandleResolver: Redis-backed persistent caching with bidirectional lookupsSqliteHandleResolver: SQLite-backed persistent caching with bidirectional supportProactiveRefreshResolver: Automatically refreshes cache entries before expiration- All resolvers implement
HandleResolvertrait with:resolve: Handle-to-DID resolutionpurge: Remove entries by handle or DIDset: Manually update handle-to-DID mappings
- Uses binary serialization via
HandleResolutionResultfor space efficiency - Resolution stack: Cache → ProactiveRefresh (optional) → RateLimited (optional) → Base → DNS/HTTP
- Includes resolution timing measurements for metrics
-
Binary Serialization (
src/handle_resolution_result.rs)- Compact storage format using bincode
- Strips DID prefixes for did:web and did:plc methods
- Stores: timestamp (u64), method type (i16), payload (String)
-
Queue System (
src/queue/)- Supports MPSC (in-process), Redis, SQLite, and no-op adapters
HandleResolutionWorkitems processed asynchronously- Redis uses reliable queue pattern (LPUSH/RPOPLPUSH/LREM)
- SQLite provides persistent queue with work shedding capabilities
-
HTTP Server (
src/http/)- XRPC endpoints for AT Protocol compatibility
- Health check endpoint
- Static file serving from configurable directory (default: www)
- Serves .well-known files as static content
- CORS headers support for cross-origin requests
- Cache-Control headers with configurable max-age and stale directives
- ETag support with configurable seed for cache invalidation
-
Metrics System (
src/metrics.rs)- Pluggable metrics publishing with StatsD support
- Tracks counters, gauges, and timings
- Configurable tags for environment/service identification
- No-op adapter for development environments
- Metrics for Jetstream event processing
-
Jetstream Consumer (
src/jetstream_handler.rs)- Consumes AT Protocol firehose events via WebSocket
- Processes Account events (purges deleted/deactivated accounts)
- Processes Identity events (updates handle-to-DID mappings)
- Automatic reconnection with exponential backoff
- Comprehensive metrics for event processing
- Spawned as cancellable task using task manager
Key Technical Details#
DID Method Types#
did:web: Web-based DIDs, prefix stripped for storagedid:plc: PLC directory DIDs, prefix stripped for storage- Other DID methods stored with full identifier
Redis Integration#
- Bidirectional Caching:
- Stores both handle→DID and DID→handle mappings
- Uses MetroHash64 for key generation
- Binary data storage for efficiency
- Automatic synchronization of both directions
- Queuing: Reliable queue with processing/dead letter queues
- Key Prefixes: Configurable via
QUEUE_REDIS_PREFIXenvironment variable
Handle Resolution Flow#
- Check cache (Redis/SQLite/in-memory based on configuration)
- If cache miss and rate limiting enabled:
- Acquire semaphore permit (with optional timeout)
- If timeout configured and exceeded, return error
- Perform DNS TXT lookup or HTTP well-known query
- Cache result with appropriate TTL in both directions (handle→DID and DID→handle)
- Return DID or error
Cache Management Operations#
- Purge: Removes entries by either handle or DID
- Uses
atproto_identity::resolve::parse_inputfor identifier detection - Removes both handle→DID and DID→handle mappings
- Chains through all resolver layers
- Uses
- Set: Manually updates handle-to-DID mappings
- Updates both directions in cache
- Normalizes handles to lowercase
- Chains through all resolver layers
Environment Variables#
Required#
HTTP_EXTERNAL: External hostname for service endpoints (e.g.,localhost:3007)
Optional - Core Configuration#
HTTP_PORT: Server port (default: 8080)PLC_HOSTNAME: PLC directory hostname (default: plc.directory)RUST_LOG: Logging level (e.g., debug, info)STATIC_FILES_DIR: Directory for serving static files (default: www)
Optional - Caching#
REDIS_URL: Redis connection URL for cachingSQLITE_URL: SQLite database URL for caching (e.g.,sqlite:./quickdid.db)CACHE_TTL_MEMORY: TTL for in-memory cache in seconds (default: 600)CACHE_TTL_REDIS: TTL for Redis cache in seconds (default: 7776000)CACHE_TTL_SQLITE: TTL for SQLite cache in seconds (default: 7776000)
Optional - Queue Configuration#
QUEUE_ADAPTER: Queue type - 'mpsc', 'redis', 'sqlite', 'noop', or 'none' (default: mpsc)QUEUE_REDIS_PREFIX: Redis key prefix for queues (default: queue:handleresolver:)QUEUE_WORKER_ID: Worker ID for queue operations (default: worker1)QUEUE_BUFFER_SIZE: Buffer size for MPSC queue (default: 1000)QUEUE_SQLITE_MAX_SIZE: Max queue size for SQLite work shedding (default: 10000)QUEUE_REDIS_TIMEOUT: Redis blocking timeout in seconds (default: 5)QUEUE_REDIS_DEDUP_ENABLED: Enable queue deduplication to prevent duplicate handles (default: false)QUEUE_REDIS_DEDUP_TTL: TTL for deduplication keys in seconds (default: 60)
Optional - Rate Limiting#
RESOLVER_MAX_CONCURRENT: Maximum concurrent handle resolutions (default: 0 = disabled)RESOLVER_MAX_CONCURRENT_TIMEOUT_MS: Timeout for acquiring rate limit permit in ms (default: 0 = no timeout)
Optional - HTTP Cache Control#
CACHE_MAX_AGE: Max-age for Cache-Control header in seconds (default: 86400)CACHE_STALE_IF_ERROR: Stale-if-error directive in seconds (default: 172800)CACHE_STALE_WHILE_REVALIDATE: Stale-while-revalidate directive in seconds (default: 86400)CACHE_MAX_STALE: Max-stale directive in seconds (default: 86400)ETAG_SEED: Seed value for ETag generation (default: application version)
Optional - Metrics#
METRICS_ADAPTER: Metrics adapter type - 'noop' or 'statsd' (default: noop)METRICS_STATSD_HOST: StatsD host and port (required when METRICS_ADAPTER=statsd, e.g., localhost:8125)METRICS_STATSD_BIND: Bind address for StatsD UDP socket (default: [::]:0 for IPv6, can use 0.0.0.0:0 for IPv4)METRICS_PREFIX: Prefix for all metrics (default: quickdid)METRICS_TAGS: Comma-separated tags (e.g., env:prod,service:quickdid)
Optional - Proactive Refresh#
PROACTIVE_REFRESH_ENABLED: Enable proactive cache refreshing (default: false)PROACTIVE_REFRESH_THRESHOLD: Refresh when TTL remaining is below this threshold (0.0-1.0, default: 0.8)
Optional - Jetstream Consumer#
JETSTREAM_ENABLED: Enable Jetstream consumer for real-time cache updates (default: false)JETSTREAM_HOSTNAME: Jetstream WebSocket hostname (default: jetstream.atproto.tools)
Error Handling#
All error strings must use this format:
error-quickdid-<domain>-<number> <message>: <details>
Current error domains and examples:
config: Configuration errors (e.g., error-quickdid-config-1 Missing required environment variable)resolve: Handle resolution errors (e.g., error-quickdid-resolve-1 Failed to resolve subject)queue: Queue operation errors (e.g., error-quickdid-queue-1 Failed to push to queue)cache: Cache-related errors (e.g., error-quickdid-cache-1 Redis pool creation failed)result: Serialization errors (e.g., error-quickdid-result-1 System time error)task: Task processing errors (e.g., error-quickdid-task-1 Queue adapter health check failed)
Errors should be represented as enums using the thiserror library.
Avoid creating new errors with the anyhow!(...) or bail!(...) macro.
Testing#
Running Tests#
# Run all tests
cargo test
# Run with Redis integration tests
TEST_REDIS_URL=redis://localhost:6379 cargo test
# Run specific test module
cargo test handle_resolver::tests
Test Coverage Areas#
- Handle resolution with various DID methods
- Binary serialization/deserialization
- Redis caching and expiration with bidirectional lookups
- Queue processing logic
- HTTP endpoint responses
- Jetstream event handler processing
- Purge and set operations across resolver layers
Development Patterns#
Error Handling#
- Uses strongly-typed errors with
thiserrorfor all modules - Each error has a unique identifier following the pattern
error-quickdid-<domain>-<number> - Graceful fallbacks when Redis/SQLite is unavailable
- Detailed tracing for debugging
- Avoid using
anyhow!()orbail!()macros - use proper error types instead
Performance Optimizations#
- Binary serialization reduces storage by ~40%
- MetroHash64 for fast key generation
- Connection pooling for Redis
- Configurable TTLs for cache entries
- Rate limiting via semaphore-based concurrency control
- HTTP caching with ETag and Cache-Control headers
- Resolution timing metrics for performance monitoring
Code Style#
- Follow existing Rust idioms and patterns
- Use
tracingfor logging, notprintln! - Prefer
Arcfor shared state across async tasks - Handle errors explicitly, avoid
.unwrap()in production code - Use
httpdatecrate for HTTP date formatting (notchrono)
Common Tasks#
Adding a New DID Method#
- Update
DidMethodTypeenum inhandle_resolution_result.rs - Modify
parse_did()andto_did()methods - Add test cases for the new method type
Modifying Cache TTL#
- For in-memory: Set
CACHE_TTL_MEMORYenvironment variable - For Redis: Set
CACHE_TTL_REDISenvironment variable - For SQLite: Set
CACHE_TTL_SQLITEenvironment variable
Configuring Metrics#
- Set
METRICS_ADAPTER=statsdandMETRICS_STATSD_HOST=localhost:8125 - Configure tags with
METRICS_TAGS=env:prod,service:quickdid - Use Telegraf + TimescaleDB for aggregation (see
docs/telegraf-timescaledb-metrics-guide.md) - Railway deployment resources available in
railway-resources/telegraf/
Debugging Resolution Issues#
- Enable debug logging:
RUST_LOG=debug - Check Redis cache:
- Handle lookup:
redis-cli GET "handle:<hash>" - DID lookup:
redis-cli GET "handle:<hash>"(same key format)
- Handle lookup:
- Check SQLite cache:
sqlite3 quickdid.db "SELECT * FROM handle_resolution_cache;" - Monitor queue processing in logs
- Check rate limiting: Look for "Rate limit permit acquisition timed out" errors
- Verify DNS/HTTP connectivity to AT Protocol infrastructure
- Monitor metrics for resolution timing and cache hit rates
- Check Jetstream consumer status:
- Look for "Jetstream consumer" log entries
- Monitor
jetstream.*metrics - Check reconnection attempts in logs
Dependencies#
atproto-identity: Core AT Protocol identity resolutionatproto-jetstream: AT Protocol Jetstream event consumerbincode: Binary serializationdeadpool-redis: Redis connection poolingmetrohash: Fast non-cryptographic hashingtokio: Async runtimeaxum: Web frameworkhttpdate: HTTP date formatting (replacing chrono)cadence: StatsD metrics clientthiserror: Error handling