When users submit a contact form, they now receive a confirmation email acknowlledging receipt of their message. The backend also continues to send a notification email to the configured recipient. If the backend fails to send the acknowledgement email to the sender, it will assume the email is not valid and will therefore not transmit the contact request to the configured recipient. Changes: - Refactor `send_email()` to `send_emails()` that sends two emails: - Confirmation email from the submitter - Notification email to the configured recipient - Add `From<T>` implementations of various errors for new error type `ContactError`. - Errors now return a translation identifier for the frontend.
12 KiB
Table of Contents
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 - Email sending is tested using lettre's
StubTransportfor mocking SMTP operations
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
│ ├── errors.rs # Error type re-exports
│ ├── middleware/ # Custom middleware
│ │ ├── mod.rs # Middleware module
│ │ └── rate_limit.rs # Rate limiting middleware
│ └── route/ # API route handlers
│ ├── mod.rs # Route organization
│ ├── contact/ # Contact form module
│ │ ├── mod.rs # Contact form endpoint
│ │ └── errors.rs # Contact form error types
│ ├── 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.