Architecture¶
Navigator v0.11.0 represents a significant architectural evolution from a single-file implementation to a modular, well-tested Go application with clear separation of concerns.
Design Principles¶
Navigator's architecture is built on these core principles:
- Focused Modules - Each package has a single, well-defined responsibility
- High Test Coverage - 81.2% test coverage across all internal packages
- Cross-Platform - Native support for Unix, Linux, macOS, and Windows
- Single Binary - Despite modular structure, still deploys as one executable
- Clear Dependencies - Minimal external dependencies, well-organized internal packages
Package Structure¶
HTTP Layer¶
internal/server/
¶
Purpose: HTTP request handling, routing, and response generation
Key Components:
handler.go
(343 LOC) - Main HTTP handler and routing logicstatic.go
(263 LOC) - Static file serving with try_files behaviormime.go
(30 LOC) - MIME type detectionaccess_log.go
(119 LOC) - Structured access logging
Responsibilities:
- Request routing and dispatching
- Static file serving with caching
- WebSocket connection upgrades
- Access logging with metadata
Test Coverage: 84.5%
Process Management¶
internal/process/
¶
Purpose: Web application and managed process lifecycle
Key Components:
webapp.go
(351 LOC) - Web app instance management and idle monitoringprocess_starter.go
(187 LOC) - Framework detection and process startupport_allocator.go
(34 LOC) - Dynamic TCP port allocationmanager.go
- External process management (Redis, Sidekiq, workers)hooks.go
- Lifecycle hook execution
Responsibilities:
- On-demand web app startup
- Framework detection (Rails, Django, Node.js)
- Port allocation in configured range
- Process health monitoring
- Idle timeout and cleanup
- Managed process supervision
- WebSocket connection tracking
Test Coverage: 75.0%
Reverse Proxy¶
internal/proxy/
¶
Purpose: Reverse proxy with Fly.io integration
Key Components:
- Proxy request forwarding to tenant apps and reverse proxies
- Fly-Replay header generation for regional routing
- Sticky session support with cookies
- Large request detection and fallback for Fly-Replay
- WebSocket support with optional connection tracking
Responsibilities:
- Proxying requests to web apps (no retry - health checks ensure readiness)
- Smart region routing with retry fallback (Fly-Replay)
- Session affinity (sticky sessions)
- Maintenance page serving
Test Coverage: 88.1%
Configuration¶
internal/config/
¶
Purpose: YAML configuration loading and validation
Key Components:
- Configuration file parsing
- Template variable substitution
- Validation and defaults
- Hot reload support (SIGHUP)
Responsibilities:
- YAML file parsing
- Environment variable expansion
- Tenant-specific configuration
- Configuration validation
- Live reload handling
Test Coverage: 83.9%
Authentication¶
internal/auth/
¶
Purpose: HTTP Basic Authentication with htpasswd
Key Components:
- htpasswd file parsing (APR1, bcrypt, SHA)
- Pattern-based authentication exclusions
- Realm management
- Authorization header processing
Responsibilities:
- Credential verification
- Pattern matching for auth exclusions
- HTTP 401 response generation
- Multiple hash format support
Test Coverage: 78.4%
Idle Management¶
internal/idle/
¶
Purpose: Fly.io machine auto-suspend/stop
Key Components:
manager.go
- Idle detection and action triggeringsignals_unix.go
- Unix/Linux/macOS signal handling (SIGTSTP, SIGTERM)signals_windows.go
- Windows graceful shutdown (os.Exit)
Responsibilities:
- Request activity tracking
- Idle timeout detection
- Machine suspend (Unix: SIGTSTP)
- Machine stop (Unix: SIGTERM, Windows: os.Exit)
- Resume hook execution
- Platform-specific signal handling
Test Coverage: 74.3%
Utility Packages¶
internal/errors/
¶
Purpose: Domain-specific error constructors
Components:
- Error constructor functions for all domain areas
- Proper error wrapping with
%w
for error chains - Consistent error message formatting
Usage Example:
return errors.ErrTenantNotFound("tenant-name")
return errors.ErrPIDFileRead("/path/to/file", err)
return errors.ErrNoAvailablePorts(4000, 4099)
Test Coverage: 86.7%
internal/logging/
¶
Purpose: Structured logging helper functions
Components:
- 24 logging helper functions
- Consistent slog-based structured logging
- Categorized by domain (Request, Proxy, Process, WebApp, etc.)
Usage Example:
logging.LogWebAppStart(tenant, port, runtime, server, args)
logging.LogProxyRequest(method, path, target)
logging.LogServerReady(host, port)
Benefits:
- Reduces multi-line slog calls to single-line functions
- Consistent logging format across codebase
- Easier to maintain and update logging patterns
Test Coverage: 100%
internal/utils/
¶
Purpose: Common utility functions
Components:
- Duration parsing with automatic error logging
- Environment variable handling
- Fly.io context detection
- Time utilities
Key Functions:
Test Coverage: 77.9%
Request Flow¶
Here's how Navigator handles an incoming HTTP request:
1. HTTP Request
│
├──> [internal/server/handler.go]
│ │
│ ├──> Check authentication
│ │ └──> [internal/auth/] - Verify credentials
│ │
│ ├──> Apply rewrite rules
│ │ └──> Check Fly-Replay requirements
│ │
│ ├──> Try static files
│ │ └──> [internal/server/static.go] - Serve from public/
│ │
│ └──> Proxy to web app
│ │
│ ├──> [internal/process/webapp.go] - Get or start app
│ │ │
│ │ └──> [internal/process/process_starter.go] - Start if needed
│ │ │
│ │ ├──> [internal/process/port_allocator.go] - Find port
│ │ └──> Execute framework-specific command
│ │
│ └──> [internal/proxy/] - Forward request
│ │
│ ├──> Add Fly-Replay headers if needed
│ ├──> Handle sticky sessions
│ └──> Retry on connection errors
│
└──> [internal/server/access_log.go] - Log request
Lifecycle Management¶
Startup Sequence¶
1. Parse command-line arguments
2. Load configuration [internal/config/]
3. Initialize idle manager [internal/idle/]
4. Initialize app manager [internal/process/]
5. Start managed processes [internal/process/manager.go]
6. Execute server start hooks
7. Start HTTP server [internal/server/]
8. Execute server ready hooks
9. Begin accepting requests
Shutdown Sequence¶
1. Receive SIGTERM/SIGINT
2. Stop accepting new requests
3. Wait for active requests (graceful shutdown)
4. Execute server idle hooks (if Fly.io machine idle)
5. Stop web applications [internal/process/webapp.go]
└──> Execute tenant stop hooks
6. Stop managed processes [internal/process/manager.go]
7. Clean up PID files
8. Exit
Configuration Reload (SIGHUP)¶
1. Receive SIGHUP signal
2. Parse new configuration [internal/config/]
3. Update app manager [internal/process/webapp.go]
└──> Update idle timeout, port range
4. Update managed processes
└──> Start new processes
└──> Keep existing processes running
5. Update routing rules [internal/server/]
6. Continue serving requests (zero downtime)
Concurrency Model¶
Navigator uses Go's concurrency primitives for safe multi-threaded operation:
Request Handling¶
- Goroutine per request - Standard Go HTTP server model
- No shared mutable state in handler path
- Read-only configuration after load
Process Management¶
- Mutex-protected maps - App registry (
internal/process/webapp.go
) - Atomic counters - Active request tracking (
internal/idle/manager.go
) - Channel-based cleanup - Graceful shutdown coordination
Idle Monitoring¶
- Per-app goroutines - Each web app has dedicated idle monitor
- Time-based timers - Idle timeout detection
- Condition variables - Resume hook synchronization
Testing Strategy¶
Navigator achieves 81.2% test coverage through:
Unit Tests¶
- Package-level tests - Each package has
*_test.go
files - Mock-based testing - Test mode flags prevent dangerous operations
- Table-driven tests - Comprehensive scenario coverage
Integration Tests¶
- HTTP round-trip tests - Full request/response cycles
- Process lifecycle tests - App startup and shutdown
- Configuration reload tests - SIGHUP handling
Platform-Specific Tests¶
- Build tags - Unix vs Windows test separation
- Signal handling tests - Test mode prevents actual signals
- Cross-compilation - CI tests all platforms
Performance Characteristics¶
Memory Usage¶
- Base process: ~20MB (vs ~100MB+ for nginx + Passenger)
- Per web app: ~50-200MB (Rails app dependent)
- Per managed process: Varies by process type
Latency¶
- Static files: <1ms (direct filesystem serving)
- Proxy overhead: ~1-2ms (reverse proxy to web app)
- App startup: 2-10s (depends on framework)
Scalability¶
- Concurrent requests: Thousands (Go HTTP server)
- Web apps: 10-100 per Navigator instance (config dependent)
- Managed processes: Limited by system resources
Evolution from v0.7 to v0.11¶
Before (v0.7.1) - Single File¶
cmd/navigator/main.go (3842 LOC)
├── All HTTP handling
├── All process management
├── All configuration
├── All authentication
└── All proxy logic
Issues: - Hard to test specific components - Difficult to maintain - Low test coverage (~65%) - Code duplication
After (v0.11.0) - Modular¶
cmd/navigator-refactored/main.go (280 LOC)
├── internal/server/ (84.5% coverage)
├── internal/process/ (75.0% coverage)
├── internal/proxy/ (88.1% coverage)
├── internal/config/ (83.9% coverage)
├── internal/auth/ (78.4% coverage)
├── internal/idle/ (74.3% coverage)
├── internal/errors/ (86.7% coverage)
├── internal/logging/ (100% coverage)
└── internal/utils/ (77.9% coverage)
Benefits: - ✅ Higher test coverage (81.2% overall) - ✅ Easier to maintain and extend - ✅ Clear separation of concerns - ✅ Reduced code duplication - ✅ Better error handling - ✅ Cross-platform support
Key Metrics¶
Metric | v0.7.1 | v0.11.0 | Improvement |
---|---|---|---|
Test Coverage | ~65% | 81.2% | +16.2% |
Main File Size | 3842 LOC | 280 LOC | -92.7% |
Packages | 1 | 9 | +8 |
Platform Support | Unix only | Unix + Windows | Full cross-platform |
Largest File | 3842 LOC | 351 LOC | -90.9% |
Design Decisions¶
Why Internal Packages?¶
- Encapsulation: Internal packages cannot be imported by external code
- Clear API: Only exported types and functions are public
- Refactoring freedom: Internal implementation can change without breaking imports
Why Platform-Specific Files?¶
- Build constraints:
//go:build unix
and//go:build windows
- Native behavior: Use appropriate OS primitives
- Clean compilation: No platform-specific code in shared files
Why Helper Packages (errors, logging)?¶
- DRY principle: Eliminate 271 repetitive logging calls
- Consistency: Standardized error and log formats
- Maintainability: Update format in one place
Future Architecture Considerations¶
Potential areas for further improvement:
- Plugin system - Dynamic framework support
- Metrics package - Prometheus/OpenTelemetry integration
- Health check package - Dedicated health endpoint handling
- Cache layer - In-memory caching for static content
- Rate limiting - Per-tenant request rate limiting
See Also¶
- REFACTORING.md - Detailed refactoring history
- CLAUDE.md - Development guidelines
- Configuration Reference - YAML configuration options
- Getting Started - Installation and first app