Client Implementation Strategy #
Purpose #
This document outlines the design philosophy and implementation strategy for the ATP Keyserver client library. It serves as a roadmap for contributors and explains key architectural decisions.
For protocol details, see ENCRYPTION_PROTOCOL.md. For security guidelines, see SECURITY.md. For usage instructions, see Client README.
Design Philosophy #
Client-Side Encryption #
Decision: Cryptographic operations (encryption/decryption) happen on the client, not the server.
Rationale:
- True end-to-end encryption: Server never sees plaintext
- Better performance: <1ms local crypto vs 100-500ms server round-trip
- Lower server load: 50% less CPU, 97% less bandwidth
- Aligned with ATProto: Decentralized architecture, user sovereignty
- Cost efficiency: Scales with user devices, not server capacity
Trade-offs:
- Clients must handle cryptography correctly
- Requires distributing crypto library (~52KB)
- Key caching responsibility on client
Alternative considered: Server-side encryption (rejected due to trust model and performance)
Single Package Architecture #
Decision: One npm package @atpkeyserver/client with optional imports via subpath exports.
Rationale:
- Crypto library (@noble/ciphers) is ~52KB and represents 90% of bundle
- High-level client adds only ~8KB on top of crypto functions
- Maintenance overhead of separate packages not justified by small size difference
- Subpath exports allow tree-shaking for minimal bundle impact
Package structure:
@atpkeyserver/client
├── /crypto → Just crypto functions (52KB)
├── /client → Just KeyserverClient (60KB, includes crypto)
└── / (main) → Both exports (60KB, tree-shaken)
Alternative considered: Separate @atpkeyserver/client-crypto and @atpkeyserver/client packages (rejected due to minimal size benefit)
Service Auth Integration #
Decision: Client library abstracts service auth token management via callback pattern.
Rationale:
- PDS clients vary by platform (@atproto/api, custom implementations)
- Callback pattern allows any PDS client integration
- Client library handles token caching automatically
- Separates concerns: client lib = crypto + keyserver API, user provides = PDS auth
Implementation:
new KeyserverClient({
keyserverDid: 'did:web:keyserver.example.com',
getServiceAuthToken: async (aud, lxm) => {
// User provides: obtain token from their PDS client
return await pdsClient.getServiceAuth({ aud, lxm })
}
})
See ENCRYPTION_PROTOCOL.md for service auth details.
Component Architecture #
Core Components #
1. Crypto Module (crypto.ts)
- XChaCha20-Poly1305 encryption/decryption
- Uses @noble/ciphers library
- Stateless, pure functions
- No network dependencies
2. KeyserverClient (client.ts)
- HTTP client for keyserver API
- Service auth token management
- Key caching with TTL
- Error handling and retries
3. Cache Module (cache.ts)
- In-memory LRU cache
- Separate TTLs for active/historical keys
- Service auth token caching
- Automatic expiry
4. Service Auth Module (service-auth.ts)
- Token cache management
- Expiry checking with safety margin
- Automatic refresh logic
5. Types Module (types.ts)
- TypeScript interfaces
- Configuration types
- Response types
6. Errors Module (errors.ts)
- Custom error classes
- Structured error handling
- HTTP status code mapping
Dependency Graph #
index.ts
├── crypto.ts
│ └── @noble/ciphers
├── client.ts
│ ├── crypto.ts
│ ├── cache.ts
│ ├── service-auth.ts
│ ├── types.ts
│ └── errors.ts
├── types.ts
└── errors.ts
Key Features #
Automatic Caching #
Key Caching:
- Active keys: 1 hour TTL (may change with rotation)
- Historical keys: 24 hour TTL (immutable)
- LRU eviction when max size reached
Service Auth Token Caching:
- 60 second TTL (or server-specified
exp) - Refresh when <10 seconds remain
- Memory-only storage
Benefits:
- Reduces keyserver load
- Improves client performance
- Handles token refresh automatically
See SECURITY.md for security considerations.
Request Deduplication #
Multiple simultaneous requests for the same key are collapsed into a single network request:
// Both calls trigger only one network request
const [key1, key2] = await Promise.all([
client.getGroupKey('did:plc:abc#followers'),
client.getGroupKey('did:plc:abc#followers')
])
Automatic Retry #
Network failures retry with exponential backoff:
- Retry on: 5xx errors, network timeouts
- Don't retry on: 4xx errors (client errors are permanent)
- Max retries: 3 attempts
- Backoff: 100ms, 200ms, 400ms
Type Safety #
Full TypeScript support:
- Strict typing for all APIs
- IntelliSense support
- Compile-time error detection
- Generated type definitions
Bundle Size Analysis #
Tree-shaking eliminates unused code. Actual bundle sizes depend on what you import:
Scenario 1: Crypto Functions Only #
import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto'
Bundled: ~52KB (minified + gzipped ~15KB)
- @noble/ciphers: 50KB
- crypto.ts: 2KB
Use case: You handle keyserver API calls manually, just need crypto.
Scenario 2: Full Client #
import { KeyserverClient } from '@atpkeyserver/client'
Bundled: ~60KB (minified + gzipped ~18KB)
- @noble/ciphers: 50KB
- Client code: 10KB (includes crypto, caching, HTTP, errors)
Use case: Full-featured client with automatic caching and token management.
Scenario 3: Both #
import { KeyserverClient, encryptMessage } from '@atpkeyserver/client'
Bundled: ~60KB (same as scenario 2)
- Client includes crypto, no duplication
Use case: Mix high-level and low-level APIs as needed.
Bundle Optimization #
Automatic:
- Tree-shaking removes unused exports
- Minification reduces code size
- Gzip compresses for network transfer
Manual optimization:
- Use subpath imports (
/crypto,/client) for smaller bundles - Import only what you need
- Modern bundlers (webpack, vite, rollup) handle this automatically
Conclusion: Crypto library dominates bundle size. Single package vs separate packages saves only ~8KB, not worth maintenance complexity.
Implementation Phases #
Phase 1: Foundation (Complete) #
Phase 2: Client Library (Complete) #
Phase 3: Documentation (Complete) #
Phase 4: Testing #
Phase 5: Polish #
Success Metrics #
Performance Targets #
| Operation | Target | Measurement |
|---|---|---|
| Encryption | <1ms | Local crypto operation |
| Decryption (cached key) | <5ms | Cache lookup + crypto |
| Decryption (cache miss) | <200ms | Network fetch + crypto |
| Service auth (cached) | <1ms | Cache lookup |
| Service auth (cache miss) | <100ms | PDS round-trip |
Developer Experience #
Security #
Code Quality #
Testing Strategy #
Unit Tests #
Crypto module:
- Encrypt/decrypt round-trip
- Nonce uniqueness
- AAD binding
- Error handling
Cache module:
- TTL expiry
- LRU eviction
- Get/set operations
- Clear operation
Service auth module:
- Token expiry checking
- Refresh logic
- Cache key generation
Integration Tests #
Client:
- Key fetch with caching
- Service auth integration
- Retry logic
- Error handling
E2E:
- Full encryption flow (PDS → keyserver → encrypt → store)
- Full decryption flow (fetch → keyserver → decrypt → display)
- Key rotation handling
Mock Server #
Build lightweight mock keyserver for testing:
- Simulates auth verification
- Returns mock keys
- Configurable latency
- Error injection
Open Questions #
Rate Limiting #
Question: Should client enforce rate limits?
Options:
- No client-side limits (rely on server)
- Simple debouncing (prevent rapid duplicate requests)
- Full rate limit tracking (mirror server limits)
Current decision: Option 2 (debouncing via request deduplication)
Cache Persistence #
Question: Should keys be persisted to disk?
Options:
- Memory-only (current implementation)
- Encrypted disk cache (requires key derivation)
- Platform-specific secure storage (iOS Keychain, Android KeyStore)
Current decision: Memory-only for security and simplicity
Rationale:
- Simpler implementation
- Better security (no disk persistence)
- Works across all platforms
- User can implement custom cache if needed
Error Recovery #
Question: How should clients handle persistent decryption failures?
Options:
- Fail silently (hide post)
- Show error message
- Retry with exponential backoff
- Prompt user action (e.g., "Request access")
Current decision: Show error message, no retry (decryption failures are permanent)
Related Documentation #
- Encryption Protocol - Protocol specification
- Security Best Practices - Security guidelines
- API Reference - Complete endpoint reference
- Architecture - Server architecture
- Client README - Usage instructions
Contributing #
Contributions welcome! Please:
- Read existing documentation
- Follow code style (Prettier)
- Add tests for new features
- Update documentation as needed
- Submit PR with clear description
For questions, open an issue in the Codeberg repository.