phundrak.com Backend
The backend for phundrak.com, built with Rust and the Poem web framework.
Features
- RESTful API with automatic OpenAPI/Swagger documentation
- Rate limiting with configurable per-second limits using the
Generic Cell Rate Algorithm (thanks to
governor) - Contact form with SMTP email relay (supports TLS, STARTTLS, and unencrypted)
- Type-safe routing using Poem's declarative API
- Hierarchical configuration with YAML files and environment variable overrides
- Structured logging with
tracingandtracing-subscriber - Strict linting for code quality and safety
- Comprehensive testing with integration test support
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:
settings/base.yaml- Base configurationsettings/{environment}.yaml- Environment-specific (development/production)- 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
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)
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)
You can also use a .env file for local development settings.
Rate Limiting
The application includes built-in rate limiting to protect against abuse:
- Uses the Generic Cell Rate Algorithm (GCRA) via the
governorcrate - 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 Requestswhen 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.
Development
Prerequisites
Option 1: Native Development
- Rust (latest stable version recommended)
- Cargo (comes with Rust)
Option 2: Nix Development (Recommended)
- Nix with flakes enabled
- All dependencies managed automatically
Running the Server
With Cargo:
cargo run
With Nix development shell:
nix develop .#backend
cargo run
The server will start on the configured port (default: 3100).
Building
With Cargo:
For development builds:
cargo build
For optimized production builds:
cargo build --release
The compiled binary will be at target/release/backend.
With Nix:
Build the backend binary:
nix build .#backend
# Binary available at: ./result/bin/backend
Build Docker images:
# 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.
Testing
Run all tests:
cargo test
# or
just test
Run a specific test:
cargo test <test_name>
Run tests with output:
cargo test -- --nocapture
Run tests with coverage:
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
Code Quality
Linting
This project uses extremely strict Clippy linting rules:
#![deny(clippy::all)]#![deny(clippy::pedantic)]#![deny(clippy::nursery)]#![warn(missing_docs)]
Run Clippy to check for issues:
cargo clippy --all-targets
# or
just lint
All code must pass these checks before committing.
Continuous Checking with Bacon
For continuous testing and linting during development, use bacon:
bacon # Runs clippy-all by default
bacon test # Runs tests continuously
bacon clippy # Runs clippy on default target only
Press 'c' in bacon to run clippy-all.
Code Style
Error Handling
- Use
thiserrorfor custom error types - Always return
Resulttypes for fallible operations - Use descriptive error messages
Logging
Always use tracing::event! with proper target and level:
tracing::event!(
target: "backend", // or "backend::module_name"
tracing::Level::INFO,
"Message here"
);
Imports
Organize imports in three groups:
- Standard library (
std::*) - External crates (poem, serde, etc.)
- Local modules (
crate::*)
Testing Conventions
- 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
Project Structure
backend/
├── src/
│ ├── main.rs # Application entry point
│ ├── lib.rs # Library root with run() and prepare()
│ ├── startup.rs # Application builder, server setup
│ ├── settings.rs # Configuration management
│ ├── telemetry.rs # Logging and tracing setup
│ ├── middleware/ # Custom middleware
│ │ ├── mod.rs # Middleware module
│ │ └── rate_limit.rs # Rate limiting middleware
│ └── 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
├── Cargo.toml # Dependencies and metadata
└── README.md # This file
Architecture
Application Initialization Flow
main.rscallsrun()fromlib.rsrun()callsprepare()which:- Loads environment variables from
.envfile - Initializes
Settingsfrom YAML files and environment variables - Sets up telemetry/logging (unless in test mode)
- Builds the
Applicationwith optional TCP listener
- Loads environment variables from
Application::build():- Sets up OpenAPI service with all API endpoints
- Configures Swagger UI at the root path (
/) - Configures API routes under
/apiprefix - Creates server with TCP listener
- 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.
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:
# 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 onmain)<version>- Specific version (e.g.,1.0.0, from tagged commits likev1.0.0)develop- Latest development build (fromdevelopbranch)pr<number>- Pull request preview builds (e.g.,pr12)
Building Images Locally
Build with Nix (recommended for reproducibility):
nix build .#backendDockerLatest
docker load < result
docker run -p 3100:3100 localhost/phundrak/backend-rust:latest
Build with Docker directly:
# Note: This requires a Dockerfile (not included in this project)
# Use Nix builds for containerization
Docker Compose Example
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):
- Checks out the repository
- Installs Nix with flakes enabled
- Builds the Docker image using Nix for reproducibility
- Authenticates with the configured Docker registry
- 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:
# 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 usernameDOCKER_PASSWORD- Registry password or tokenCACHIX_AUTH_TOKEN- (Optional) For Nix build caching
See .github/workflows/README.md for detailed setup instructions.
License
AGPL-3.0-only - See the root repository for full license information.