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)#
- Crypto functions (encryptMessage, decryptMessage)
- Type definitions
- Package structure with subpath exports
- Build configuration (ESM + CJS)
Phase 2: Client Library (Complete)#
- KeyserverClient class
- Service auth token management
- Key caching with TTL
- HTTP client with retry logic
- Error handling
- Request deduplication
Phase 3: Documentation (Complete)#
- Protocol specification (ENCRYPTION_PROTOCOL.md)
- Security guidelines (SECURITY.md)
- API reference (API_REFERENCE.md)
- Client README with usage examples
- Minimal example implementation (basic-usage.ts)
Phase 4: Testing#
- Unit tests for crypto functions
- Integration tests for client
- Mock keyserver for testing
- E2E encryption flow test
- Cache behavior tests
- Service auth token refresh tests
Phase 5: Polish#
- Performance benchmarks
- Bundle size analysis
- Documentation review
- Example applications
- npm package publication
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#
- Complete TypeScript types
- Comprehensive JSDoc comments
- Working examples for 3+ platforms
- <5 minutes from install to first encrypted message
- Clear error messages with actionable guidance
Security#
- Zero plaintext exposure to server
- Keys cached in memory only
- Service auth tokens cached for max 60 seconds
- Proper cleanup on logout
- No key or token leakage in logs
- Audience binding prevents token misuse
- Short-lived tokens limit attack window
Code Quality#
- >80% test coverage
- Zero runtime dependencies beyond @noble/ciphers
- Tree-shakeable exports
- CommonJS + ESM support
- Works in Node.js, browser, React Native
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.