2025-11-04 09:17:18 +01:00
|
|
|
# phundrak.com Backend
|
|
|
|
|
|
|
|
|
|
The backend for [phundrak.com](https://phundrak.com), built with Rust and the [Poem](https://github.com/poem-web/poem) web framework.
|
|
|
|
|
|
|
|
|
|
## Features
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
- **RESTful API** with automatic OpenAPI/Swagger documentation
|
2025-11-04 23:57:52 +01:00
|
|
|
- **Rate limiting** with configurable per-second limits using the
|
|
|
|
|
Generic Cell Rate Algorithm (thanks to
|
|
|
|
|
[`governor`](https://github.com/boinkor-net/governor))
|
|
|
|
|
- **Contact form** with SMTP email relay (supports TLS, STARTTLS, and
|
|
|
|
|
unencrypted)
|
2025-11-04 09:17:18 +01:00
|
|
|
- **Type-safe routing** using Poem's declarative API
|
2025-11-04 23:57:52 +01:00
|
|
|
- **Hierarchical configuration** with YAML files and environment
|
|
|
|
|
variable overrides
|
2025-11-04 16:27:54 +01:00
|
|
|
- **Structured logging** with `tracing` and `tracing-subscriber`
|
2025-11-04 09:17:18 +01:00
|
|
|
- **Strict linting** for code quality and safety
|
|
|
|
|
- **Comprehensive testing** with integration test support
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
## API Endpoints
|
|
|
|
|
|
|
|
|
|
The application provides the following endpoints:
|
|
|
|
|
|
|
|
|
|
- **Swagger UI**: `/` - Interactive API documentation
|
|
|
|
|
- **OpenAPI Spec**: `/specs` - OpenAPI specification in YAML format
|
|
|
|
|
- **Health Check**: `GET /api/health` - Returns server health status
|
|
|
|
|
- **Application Metadata**: `GET /api/meta` - Returns version and build info
|
|
|
|
|
- **Contact Form**: `POST /api/contact` - Submit contact form (relays to SMTP)
|
|
|
|
|
|
|
|
|
|
## Configuration
|
|
|
|
|
|
|
|
|
|
Configuration is loaded from multiple sources in order of precedence:
|
|
|
|
|
|
|
|
|
|
1. `settings/base.yaml` - Base configuration
|
|
|
|
|
2. `settings/{environment}.yaml` - Environment-specific (development/production)
|
|
|
|
|
3. Environment variables prefixed with `APP__` (e.g., `APP__APPLICATION__PORT=8080`)
|
|
|
|
|
|
|
|
|
|
The environment is determined by the `APP_ENVIRONMENT` variable (defaults to "development").
|
|
|
|
|
|
|
|
|
|
### Configuration Example
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
application:
|
|
|
|
|
port: 3100
|
|
|
|
|
version: "0.1.0"
|
|
|
|
|
|
|
|
|
|
email:
|
|
|
|
|
host: smtp.example.com
|
|
|
|
|
port: 587
|
|
|
|
|
user: user@example.com
|
|
|
|
|
from: Contact Form <noreply@example.com>
|
|
|
|
|
password: your_password
|
|
|
|
|
recipient: Admin <admin@example.com>
|
|
|
|
|
starttls: true # Use STARTTLS (typically port 587)
|
|
|
|
|
tls: false # Use implicit TLS (typically port 465)
|
2025-11-04 23:57:52 +01:00
|
|
|
|
|
|
|
|
rate_limit:
|
|
|
|
|
enabled: true # Enable/disable rate limiting
|
|
|
|
|
burst_size: 10 # Maximum requests allowed in time window
|
|
|
|
|
per_seconds: 60 # Time window in seconds (100 req/60s = ~1.67 req/s)
|
2025-11-04 16:27:54 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You can also use a `.env` file for local development settings.
|
|
|
|
|
|
2025-11-04 23:57:52 +01:00
|
|
|
### Rate Limiting
|
|
|
|
|
|
|
|
|
|
The application includes built-in rate limiting to protect against abuse:
|
|
|
|
|
|
|
|
|
|
- Uses the **Generic Cell Rate Algorithm (GCRA)** via the `governor` crate
|
|
|
|
|
- **In-memory rate limiting** - no external dependencies like Redis required
|
|
|
|
|
- **Configurable limits** via YAML configuration or environment variables
|
|
|
|
|
- **Per-second rate limiting** with burst support
|
|
|
|
|
- Returns `429 Too Many Requests` when limits are exceeded
|
|
|
|
|
|
|
|
|
|
Default configuration: 100 requests per 60 seconds (approximately 1.67 requests per second with burst capacity).
|
|
|
|
|
|
|
|
|
|
To disable rate limiting, set `rate_limit.enabled: false` in your configuration.
|
|
|
|
|
|
2025-11-04 09:17:18 +01:00
|
|
|
## Development
|
|
|
|
|
|
|
|
|
|
### Prerequisites
|
|
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
**Option 1: Native Development**
|
2025-11-04 09:17:18 +01:00
|
|
|
- Rust (latest stable version recommended)
|
|
|
|
|
- Cargo (comes with Rust)
|
|
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
**Option 2: Nix Development (Recommended)**
|
|
|
|
|
- [Nix](https://nixos.org/download) with flakes enabled
|
|
|
|
|
- All dependencies managed automatically
|
|
|
|
|
|
2025-11-04 09:17:18 +01:00
|
|
|
### Running the Server
|
|
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
**With Cargo:**
|
|
|
|
|
```bash
|
|
|
|
|
cargo run
|
|
|
|
|
```
|
2025-11-04 09:17:18 +01:00
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
**With Nix development shell:**
|
2025-11-04 09:17:18 +01:00
|
|
|
```bash
|
2025-11-05 01:25:55 +01:00
|
|
|
nix develop .#backend
|
2025-11-04 09:17:18 +01:00
|
|
|
cargo run
|
|
|
|
|
```
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
The server will start on the configured port (default: 3100).
|
2025-11-04 09:17:18 +01:00
|
|
|
|
|
|
|
|
### Building
|
|
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
**With Cargo:**
|
2025-11-04 09:17:18 +01:00
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
For development builds:
|
2025-11-04 09:17:18 +01:00
|
|
|
```bash
|
|
|
|
|
cargo build
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For optimized production builds:
|
|
|
|
|
```bash
|
|
|
|
|
cargo build --release
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The compiled binary will be at `target/release/backend`.
|
|
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
**With Nix:**
|
|
|
|
|
|
|
|
|
|
Build the backend binary:
|
|
|
|
|
```bash
|
|
|
|
|
nix build .#backend
|
|
|
|
|
# Binary available at: ./result/bin/backend
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Build Docker images:
|
|
|
|
|
```bash
|
|
|
|
|
# Build versioned Docker image (e.g., 0.1.0)
|
|
|
|
|
nix build .#backendDocker
|
|
|
|
|
|
|
|
|
|
# Build latest Docker image
|
|
|
|
|
nix build .#backendDockerLatest
|
|
|
|
|
|
|
|
|
|
# Load into Docker
|
|
|
|
|
docker load < result
|
|
|
|
|
# Image will be available as: localhost/phundrak/backend-rust:latest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The Nix build ensures reproducible builds with all dependencies pinned.
|
|
|
|
|
|
2025-11-04 09:17:18 +01:00
|
|
|
## Testing
|
|
|
|
|
|
|
|
|
|
Run all tests:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo test
|
2025-11-04 16:27:54 +01:00
|
|
|
# or
|
|
|
|
|
just test
|
2025-11-04 09:17:18 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Run a specific test:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo test <test_name>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Run tests with output:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo test -- --nocapture
|
|
|
|
|
```
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
Run tests with coverage:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo tarpaulin --config .tarpaulin.local.toml
|
|
|
|
|
# or
|
|
|
|
|
just coverage
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Testing Notes
|
|
|
|
|
|
|
|
|
|
- Integration tests use random TCP ports to avoid conflicts
|
|
|
|
|
- Tests use `get_test_app()` helper for consistent test setup
|
|
|
|
|
- Telemetry is automatically disabled during tests
|
|
|
|
|
- Tests are organized in `#[cfg(test)]` modules within each file
|
|
|
|
|
|
2025-11-04 09:17:18 +01:00
|
|
|
## Code Quality
|
|
|
|
|
|
|
|
|
|
### Linting
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
This project uses extremely strict Clippy linting rules:
|
2025-11-04 09:17:18 +01:00
|
|
|
|
|
|
|
|
- `#![deny(clippy::all)]`
|
|
|
|
|
- `#![deny(clippy::pedantic)]`
|
|
|
|
|
- `#![deny(clippy::nursery)]`
|
2025-11-04 16:27:54 +01:00
|
|
|
- `#![warn(missing_docs)]`
|
2025-11-04 09:17:18 +01:00
|
|
|
|
|
|
|
|
Run Clippy to check for issues:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cargo clippy --all-targets
|
2025-11-04 16:27:54 +01:00
|
|
|
# or
|
|
|
|
|
just lint
|
2025-11-04 09:17:18 +01:00
|
|
|
```
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
All code must pass these checks before committing.
|
|
|
|
|
|
2025-11-04 09:17:18 +01:00
|
|
|
### Continuous Checking with Bacon
|
|
|
|
|
|
|
|
|
|
For continuous testing and linting during development, use [bacon](https://dystroy.org/bacon/):
|
|
|
|
|
|
|
|
|
|
```bash
|
2025-11-04 16:27:54 +01:00
|
|
|
bacon # Runs clippy-all by default
|
|
|
|
|
bacon test # Runs tests continuously
|
|
|
|
|
bacon clippy # Runs clippy on default target only
|
2025-11-04 09:17:18 +01:00
|
|
|
```
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
Press 'c' in bacon to run clippy-all.
|
2025-11-04 09:17:18 +01:00
|
|
|
|
|
|
|
|
## Code Style
|
|
|
|
|
|
|
|
|
|
### Error Handling
|
|
|
|
|
|
|
|
|
|
- Use `thiserror` for custom error types
|
|
|
|
|
- Always return `Result` types for fallible operations
|
|
|
|
|
- Use descriptive error messages
|
|
|
|
|
|
|
|
|
|
### Logging
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
Always use `tracing::event!` with proper target and level:
|
2025-11-04 09:17:18 +01:00
|
|
|
|
|
|
|
|
```rust
|
2025-11-04 16:27:54 +01:00
|
|
|
tracing::event!(
|
|
|
|
|
target: "backend", // or "backend::module_name"
|
|
|
|
|
tracing::Level::INFO,
|
|
|
|
|
"Message here"
|
|
|
|
|
);
|
2025-11-04 09:17:18 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Imports
|
|
|
|
|
|
|
|
|
|
Organize imports in three groups:
|
|
|
|
|
1. Standard library (`std::*`)
|
2025-11-04 16:27:54 +01:00
|
|
|
2. External crates (poem, serde, etc.)
|
|
|
|
|
3. Local modules (`crate::*`)
|
2025-11-04 09:17:18 +01:00
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
### Testing Conventions
|
2025-11-04 09:17:18 +01:00
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
- Use `#[tokio::test]` for async tests
|
|
|
|
|
- Use descriptive test names that explain what is being tested
|
|
|
|
|
- Test both success and error cases
|
|
|
|
|
- For endpoint tests, verify both status codes and response bodies
|
2025-11-04 09:17:18 +01:00
|
|
|
|
|
|
|
|
## Project Structure
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
backend/
|
|
|
|
|
├── src/
|
|
|
|
|
│ ├── main.rs # Application entry point
|
2025-11-04 16:27:54 +01:00
|
|
|
│ ├── lib.rs # Library root with run() and prepare()
|
|
|
|
|
│ ├── startup.rs # Application builder, server setup
|
|
|
|
|
│ ├── settings.rs # Configuration management
|
|
|
|
|
│ ├── telemetry.rs # Logging and tracing setup
|
2025-11-04 23:57:52 +01:00
|
|
|
│ ├── middleware/ # Custom middleware
|
|
|
|
|
│ │ ├── mod.rs # Middleware module
|
|
|
|
|
│ │ └── rate_limit.rs # Rate limiting middleware
|
2025-11-04 16:27:54 +01:00
|
|
|
│ └── route/ # API route handlers
|
|
|
|
|
│ ├── mod.rs # Route organization
|
|
|
|
|
│ ├── contact.rs # Contact form endpoint
|
|
|
|
|
│ ├── health.rs # Health check endpoint
|
|
|
|
|
│ └── meta.rs # Metadata endpoint
|
|
|
|
|
├── settings/ # Configuration files
|
|
|
|
|
│ ├── base.yaml # Base configuration
|
|
|
|
|
│ ├── development.yaml # Development overrides
|
|
|
|
|
│ └── production.yaml # Production overrides
|
2025-11-04 09:17:18 +01:00
|
|
|
├── Cargo.toml # Dependencies and metadata
|
|
|
|
|
└── README.md # This file
|
|
|
|
|
```
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
### Application Initialization Flow
|
|
|
|
|
|
|
|
|
|
1. `main.rs` calls `run()` from `lib.rs`
|
|
|
|
|
2. `run()` calls `prepare()` which:
|
|
|
|
|
- Loads environment variables from `.env` file
|
|
|
|
|
- Initializes `Settings` from YAML files and environment variables
|
|
|
|
|
- Sets up telemetry/logging (unless in test mode)
|
|
|
|
|
- Builds the `Application` with optional TCP listener
|
|
|
|
|
3. `Application::build()`:
|
|
|
|
|
- Sets up OpenAPI service with all API endpoints
|
|
|
|
|
- Configures Swagger UI at the root path (`/`)
|
|
|
|
|
- Configures API routes under `/api` prefix
|
|
|
|
|
- Creates server with TCP listener
|
|
|
|
|
4. Application runs with CORS middleware and settings injected as data
|
|
|
|
|
|
|
|
|
|
### Email Handling
|
|
|
|
|
|
|
|
|
|
The contact form supports multiple SMTP configurations:
|
|
|
|
|
- **Implicit TLS (SMTPS)** - typically port 465
|
|
|
|
|
- **STARTTLS (Always/Opportunistic)** - typically port 587
|
|
|
|
|
- **Unencrypted** (for local dev) - with or without authentication
|
|
|
|
|
|
|
|
|
|
The `SmtpTransport` is built dynamically from `EmailSettings` based on
|
|
|
|
|
TLS/STARTTLS configuration.
|
|
|
|
|
|
2025-11-05 01:25:55 +01:00
|
|
|
## Docker Deployment
|
|
|
|
|
|
|
|
|
|
### Using Pre-built Images
|
|
|
|
|
|
|
|
|
|
Docker images are automatically built and published via GitHub Actions to the configured container registry.
|
|
|
|
|
|
|
|
|
|
Pull and run the latest image:
|
|
|
|
|
```bash
|
|
|
|
|
# Pull from Phundrak Labs (labs.phundrak.com)
|
|
|
|
|
docker pull labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
|
|
|
|
|
|
|
|
|
|
# Run the container
|
|
|
|
|
docker run -d \
|
|
|
|
|
--name phundrak-backend \
|
|
|
|
|
-p 3100:3100 \
|
|
|
|
|
-e APP__APPLICATION__PORT=3100 \
|
|
|
|
|
-e APP__EMAIL__HOST=smtp.example.com \
|
|
|
|
|
-e APP__EMAIL__PORT=587 \
|
|
|
|
|
-e APP__EMAIL__USER=user@example.com \
|
|
|
|
|
-e APP__EMAIL__PASSWORD=your_password \
|
|
|
|
|
-e APP__EMAIL__FROM="Contact Form <noreply@example.com>" \
|
|
|
|
|
-e APP__EMAIL__RECIPIENT="Admin <admin@example.com>" \
|
|
|
|
|
labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Available Image Tags
|
|
|
|
|
|
|
|
|
|
The following tags are automatically published:
|
|
|
|
|
|
|
|
|
|
- `latest` - Latest stable release (from tagged commits on `main`)
|
|
|
|
|
- `<version>` - Specific version (e.g., `1.0.0`, from tagged commits like `v1.0.0`)
|
|
|
|
|
- `develop` - Latest development build (from `develop` branch)
|
|
|
|
|
- `pr<number>` - Pull request preview builds (e.g., `pr12`)
|
|
|
|
|
|
|
|
|
|
### Building Images Locally
|
|
|
|
|
|
|
|
|
|
Build with Nix (recommended for reproducibility):
|
|
|
|
|
```bash
|
|
|
|
|
nix build .#backendDockerLatest
|
|
|
|
|
docker load < result
|
|
|
|
|
docker run -p 3100:3100 localhost/phundrak/backend-rust:latest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Build with Docker directly:
|
|
|
|
|
```bash
|
|
|
|
|
# Note: This requires a Dockerfile (not included in this project)
|
|
|
|
|
# Use Nix builds for containerization
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Docker Compose Example
|
|
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
version: '3.8'
|
|
|
|
|
|
|
|
|
|
services:
|
|
|
|
|
backend:
|
|
|
|
|
image: labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
|
|
|
|
|
ports:
|
|
|
|
|
- "3100:3100"
|
|
|
|
|
environment:
|
|
|
|
|
APP__APPLICATION__PORT: 3100
|
|
|
|
|
APP__EMAIL__HOST: smtp.example.com
|
|
|
|
|
APP__EMAIL__PORT: 587
|
|
|
|
|
APP__EMAIL__USER: ${SMTP_USER}
|
|
|
|
|
APP__EMAIL__PASSWORD: ${SMTP_PASSWORD}
|
|
|
|
|
APP__EMAIL__FROM: "Contact Form <noreply@example.com>"
|
|
|
|
|
APP__EMAIL__RECIPIENT: "Admin <admin@example.com>"
|
|
|
|
|
APP__EMAIL__STARTTLS: true
|
|
|
|
|
APP__RATE_LIMIT__ENABLED: true
|
|
|
|
|
APP__RATE_LIMIT__BURST_SIZE: 10
|
|
|
|
|
APP__RATE_LIMIT__PER_SECONDS: 60
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## CI/CD Pipeline
|
|
|
|
|
|
|
|
|
|
### Automated Docker Publishing
|
|
|
|
|
|
|
|
|
|
GitHub Actions automatically builds and publishes Docker images based on repository events:
|
|
|
|
|
|
|
|
|
|
| Event Type | Trigger | Published Tags |
|
|
|
|
|
|-----------------|------------------------------|-------------------------------|
|
|
|
|
|
| Tag push | `v*.*.*` tag on `main` | `latest`, `<version>` |
|
|
|
|
|
| Branch push | Push to `develop` | `develop` |
|
|
|
|
|
| Pull request | PR opened/updated | `pr<number>` |
|
|
|
|
|
| Branch push | Push to `main` (no tag) | `latest` |
|
|
|
|
|
|
|
|
|
|
### Workflow Details
|
|
|
|
|
|
|
|
|
|
The CI/CD pipeline (`.github/workflows/publish-docker.yml`):
|
|
|
|
|
|
|
|
|
|
1. **Checks out the repository**
|
|
|
|
|
2. **Installs Nix** with flakes enabled
|
|
|
|
|
3. **Builds the Docker image** using Nix for reproducibility
|
|
|
|
|
4. **Authenticates** with the configured Docker registry
|
|
|
|
|
5. **Tags and pushes** images based on the event type
|
|
|
|
|
|
|
|
|
|
### Registry Configuration
|
|
|
|
|
|
|
|
|
|
Images are published to the registry specified by the `DOCKER_REGISTRY` environment variable in the workflow (default: `labs.phundrak.com`).
|
|
|
|
|
|
|
|
|
|
To use the published images, authenticate with the registry:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# For Phundrak Labs (labs.phundrak.com)
|
|
|
|
|
echo $GITHUB_TOKEN | docker login labs.phundrak.com -u USERNAME --password-stdin
|
|
|
|
|
|
|
|
|
|
# Pull the image
|
|
|
|
|
docker pull labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Required Secrets
|
|
|
|
|
|
|
|
|
|
The workflow requires these GitHub secrets:
|
|
|
|
|
- `DOCKER_USERNAME` - Registry username
|
|
|
|
|
- `DOCKER_PASSWORD` - Registry password or token
|
|
|
|
|
- `CACHIX_AUTH_TOKEN` - (Optional) For Nix build caching
|
|
|
|
|
|
|
|
|
|
See [.github/workflows/README.md](../.github/workflows/README.md) for detailed setup instructions.
|
|
|
|
|
|
2025-11-04 09:17:18 +01:00
|
|
|
## License
|
|
|
|
|
|
2025-11-04 16:27:54 +01:00
|
|
|
AGPL-3.0-only - See the root repository for full license information.
|