tideway 0.7.13

A batteries-included Rust web framework built on Axum for building SaaS applications quickly
docs.rs failed to build tideway-0.7.13
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

Tideway

Crates.io Documentation License

Tideway is a batteries-included Rust web framework built on Axum and Tokio. It provides opinionated defaults for building SaaS applications quickly while maintaining the performance and flexibility you expect from Rust.

Features

  • Fast & Reliable: Built on Axum and Tokio for maximum performance
  • Batteries Included: Pre-configured logging, tracing, error handling, and health checks
  • Modular Architecture: Organize your application into reusable route modules
  • Trait-Based Extensibility: Swap database, cache, session, and email implementations easily
  • Production Middleware: Compression, security headers, timeouts, and Prometheus metrics
  • Request Validation: Type-safe validation with custom validators and extractors
  • Enhanced Error Handling: Rich error responses with context, IDs, and stack traces
  • Background Jobs: In-memory and Redis-backed job queues with retry logic
  • Email: SMTP and console mailers with support for Resend, SendGrid, and more
  • Developer Experience: Alba-style testing, test fixtures, dev mode debugging tools
  • WebSocket Support: Real-time communication with connection management and broadcasting
  • Type-Safe: Full Rust type safety with excellent error messages
  • Production Ready: Graceful shutdown, request IDs, and structured logging out of the box
  • Developer Friendly: Simple, intuitive API with sensible defaults

Feature Matrix

Feature flags are opt-in unless marked Default.

Feature Module Docs Example Notes
feature-gate-errors Optional compile-time errors for missing features
feature-gate-warnings Optional warnings for missing features
macros tideway-macros / openapi docs/openapi.md examples/api_macro_example.rs Default
database database docs/database_traits.md examples/custom_database.rs Default (SeaORM)
database-sqlx database docs/database_traits.md WIP
openapi openapi docs/openapi.md examples/api_macro_example.rs Default
validation validation docs/validation.md examples/validation_example.rs
metrics metrics README.md#built-in-middleware tests/metrics_integration_test.rs
cache cache docs/caching.md examples/redis_cache.rs
cache-redis cache docs/caching.md examples/redis_cache.rs
sessions session docs/sessions.md examples/sessions_example.rs
jobs jobs docs/background_jobs.md examples/background_jobs.rs
jobs-redis jobs docs/background_jobs.md
websocket websocket docs/websockets.md examples/websocket_chat.rs
email email docs/email.md examples/email_example.rs
auth auth docs/auth.md examples/seaorm_auth.rs
auth-mfa auth::mfa docs/auth.md examples/seaorm_auth.rs
auth-breach auth::breach docs/auth.md
test-auth-bypass auth docs/auth.md tests/auth_integration_test.rs Tests only
billing billing docs/billing.md
billing-seaorm billing docs/billing.md
test-billing billing docs/billing.md tests/ Tests only
organizations organizations Docs TBD
organizations-seaorm organizations Docs TBD
organizations-billing organizations Docs TBD
test-organizations organizations tests/ Tests only
admin admin Docs TBD

Quick Start

Installation

Add Tideway to your Cargo.toml:

[dependencies]
tideway = "0.7.10"
tokio = { version = "1.48", features = ["full"] }

Hello World

use tideway::{self, App, ConfigBuilder};

#[tokio::main]
async fn main() {
    // Initialize logging
    tideway::init_tracing();

    // Create app with default configuration
    let app = App::new();

    // Start server
    app.serve().await.unwrap();
}

Manual Serving

If you want to serve Tideway with axum::serve, use the middleware-aware router:

use tideway::App;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let app = App::new();
    let router = app.into_router_with_middleware();

    let listener = TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, router).await
}

Note: into_router() does not apply the full middleware stack. Use into_router_with_middleware() whenever you serve manually.

Run your app:

cargo run

Visit http://localhost:8000/health to see the built-in health check.

CLI (Fastest Start)

Use the CLI to scaffold a minimal Tideway app:

cargo install tideway-cli
tideway new my_app
cd my_app
cargo run

Or apply the API preset:

tideway new my_app --preset api

When no flags are provided, the CLI will prompt you interactively (similar to Vite). You can disable prompts with --no-prompt.

Agent Quickstart

If you're using coding agents (Codex, Claude Code, OpenCode), start here:

  • Use tideway new my_app and follow the wizard (fastest path).
  • Add resources with tideway resource <name> --wire --db --repo --service --paginate --search.
  • Run tideway dev --fix-env to boot with env + migrations.

Agent-friendly flags:

  • --json emits machine-readable JSON lines.
  • --plan shows planned file operations without writing.

Project-specific guidance lives in SKILLS.md.

Getting Started Guide

Read the full walkthrough at docs/getting_started.md.

CLI Reference

See docs/cli.md for command examples.

Common tideway new flags:

Flag Example Purpose
--preset --preset api Apply a preset scaffold
--features --features auth,database Enable crate features
--with-config --with-config Generate config.rs / error.rs
--with-docker --with-docker Add docker-compose.yml
--with-ci --with-ci Add GitHub Actions workflow
--with-env --with-env Generate .env.example
--no-prompt --no-prompt Disable interactive prompts
--summary --summary false Hide the file summary

Core Concepts

1. Application Structure

Tideway applications are organized into layers:

src/
├── main.rs           # Application entry point
├── lib.rs           # Library exports
└── routes/          # Your application routes
    └── ...

When using Tideway as a dependency, import from the tideway crate:

use tideway::{App, ConfigBuilder, RouteModule, Result, TidewayError};

You can define modules with less boilerplate using the module! macro:

tideway::module!(
    UsersModule,
    prefix = "/api",
    routes = [
        (get, "/users", list_users),
        (post, "/users", create_user),
    ]
);

You can also group multiple methods for the same path:

tideway::module!(
    UsersModule,
    prefix = "/api",
    routes = [
        ("/users", get => list_users, post => create_user),
    ]
);

OpenAPI per module (optional):

#[cfg(feature = "openapi")]
mod openapi_docs {
    tideway::openapi_doc!(pub(crate) UsersDoc, paths(crate::routes::users::list_users));
}

#[cfg(feature = "openapi")]
let openapi = tideway::openapi_merge_module!(openapi_docs, UsersDoc);

Quick guards with ensure!:

use tideway::ensure;

ensure!(user.is_admin, TidewayError::forbidden("Admin access required"));
ensure!(user.id != target_id, "Cannot delete your own account");

See docs/error_handling.md for more examples.

Testing helpers (HTTP):

use tideway::testing::get as test_get;
use tideway::{App, RouteModule};

let app = App::new().register_module(UsersModule).into_router();
test_get(app, "/api/users")
    .execute()
    .await
    .assert_ok();

See docs/testing.md for more helpers.

2. Configuration

Configure your application with environment variables or code:

use tideway::ConfigBuilder;

let config = ConfigBuilder::new()
    .with_host("0.0.0.0")
    .with_port(3000)
    .with_log_level("debug")
    .with_max_body_size(50 * 1024 * 1024) // 50MB global limit
    .from_env()  // Override with TIDEWAY_* env vars
    .build()?;  // Returns Result<Config> - validates configuration

Environment Variables:

  • TIDEWAY_HOST - Server host (default: 0.0.0.0)
  • TIDEWAY_PORT - Server port (default: 8000)
  • TIDEWAY_LOG_LEVEL - Log level (default: info)
  • TIDEWAY_LOG_JSON - Enable JSON logging (default: false)
  • TIDEWAY_MAX_BODY_SIZE - Maximum request body size in bytes (default: 10MB)
  • RUST_LOG - Standard Rust log filter

3. Route Modules

Create modular, reusable route groups with the RouteModule trait:

use axum::{routing::get, Router};
use tideway::RouteModule;

struct UsersModule;

impl RouteModule for UsersModule {
    fn routes(&self) -> Router {
        Router::new()
            .route("/", get(list_users))
            .route("/:id", get(get_user))
    }

    fn prefix(&self) -> Option<&str> {
        Some("/api/users")
    }
}

// Register module
let app = App::new()
    .register_module(UsersModule);

Register multiple modules (and optional ones) more concisely:

let app = tideway::register_modules!(
    App::new(),
    UsersModule,
    AdminModule,
);

let app = tideway::register_modules!(
    app,
    BillingModule;
    optional: optional_module
);

If you already have a homogeneous list of modules (same type), you can also use App::register_modules(modules) with any iterator.

For optional-only modules, you can use the helper macro:

let app = tideway::register_optional_modules!(
    App::new(),
    optional_module,
);

4. Error Handling

Use TidewayError for consistent error responses:

use tideway::{Result, TidewayError, ErrorContext};
use axum::Json;

async fn get_user(id: u64) -> Result<Json<User>> {
    let user = database.find(id)
        .ok_or_else(|| {
            TidewayError::not_found("User not found")
                .with_context(
                    ErrorContext::new()
                        .with_error_id(uuid::Uuid::new_v4().to_string())
                        .with_detail(format!("User ID {} does not exist", id))
                )
        })?;

    Ok(Json(user))
}

Error Types:

  • TidewayError::not_found(msg) - 404 Not Found
  • TidewayError::bad_request(msg) - 400 Bad Request
  • TidewayError::unauthorized(msg) - 401 Unauthorized
  • TidewayError::forbidden(msg) - 403 Forbidden
  • TidewayError::internal(msg) - 500 Internal Server Error
  • TidewayError::service_unavailable(msg) - 503 Service Unavailable

Quick Guards with ensure!:

use tideway::{ensure, Result, TidewayError};

fn require_admin(user: &User) -> Result<()> {
    ensure!(user.is_admin, TidewayError::forbidden("Admin access required"));
    Ok(())
}
fn prevent_self_delete(user: &User, target_id: uuid::Uuid) -> Result<()> {
    ensure!(user.id != target_id, "Cannot delete your own account");
    Ok(())
}

Enhanced Error Responses: All errors automatically return JSON responses with:

  • Error message
  • Unique error ID for tracking
  • Optional details and context
  • Field-specific validation errors
  • Stack traces (in dev mode)
{
  "error": "Bad request: Validation failed",
  "error_id": "550e8400-e29b-41d4-a716-446655440000",
  "details": "Invalid input data",
  "field_errors": {
    "email": ["must be a valid email"],
    "age": ["must be between 18 and 100"]
  }
}

5. Request Validation

Validate request data with type-safe extractors:

use tideway::validation::{ValidatedJson, ValidatedQuery, validate_uuid};
use validator::Validate;
use serde::Deserialize;

#[derive(Deserialize, Validate)]
struct CreateUserRequest {
    #[validate(email)]
    email: String,
    #[validate(custom = "validate_uuid")]
    organization_id: String,
    #[validate(length(min = 8))]
    password: String,
}

async fn create_user(
    ValidatedJson(req): ValidatedJson<CreateUserRequest>
) -> tideway::Result<axum::Json<serde_json::Value>> {
    // req is guaranteed to be valid
    Ok(axum::Json(serde_json::json!({"status": "created"})))
}

#[derive(Deserialize, Validate)]
struct SearchQuery {
    #[validate(length(min = 1, max = 100))]
    q: String,
    #[validate(range(min = 1, max = 100))]
    limit: Option<u32>,
}

async fn search(
    ValidatedQuery(query): ValidatedQuery<SearchQuery>
) -> tideway::Result<axum::Json<serde_json::Value>> {
    // query is guaranteed to be valid
    Ok(axum::Json(serde_json::json!({"results": []})))
}

Custom Validators:

  • validate_uuid() - UUID v4 validation
  • validate_slug() - Slug format validation
  • validate_phone() - Phone number validation
  • validate_json_string() - JSON string validation
  • validate_duration() - Duration format (30s, 5m, 1h, 2d)

6. Response Helpers

Use ApiResponse for standardized JSON responses:

use tideway::{ApiResponse, PaginatedData, PaginationMeta};
use axum::Json;

async fn list_todos(page: u32) -> Json<ApiResponse<PaginatedData<Todo>>> {
    let todos = get_todos_from_db(page);
    Json(ApiResponse::paginated(todos.items, PaginationMeta {
        page,
        per_page: 20,
        total: todos.total,
    }))
}

async fn create_todo() -> tideway::Result<CreatedResponse<Todo>> {
    let todo = create_todo_in_db();
    Ok(CreatedResponse::new(todo, "/api/todos/123"))
}

Response Formats:

// Success response
{
  "success": true,
  "data": [...],
  "message": "Optional message"
}

// Paginated response
{
  "success": true,
  "data": [...],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 100
  }
}

// Created response (201)
{
  "success": true,
  "data": {...},
  "location": "/api/todos/123"
}

7. Health Checks

The built-in /health endpoint is automatically available. Customize health checks:

use tideway::health::{HealthCheck, ComponentHealth, HealthStatus};
use std::pin::Pin;

struct DatabaseHealthCheck;

impl HealthCheck for DatabaseHealthCheck {
    fn name(&self) -> &str {
        "database"
    }

    fn check(&self) -> Pin<Box<dyn Future<Output = ComponentHealth> + Send + '_>> {
        Box::pin(async {
            // Check database connection
            let is_healthy = check_db_connection().await;

            ComponentHealth {
                name: "database".to_string(),
                status: if is_healthy {
                    HealthStatus::Healthy
                } else {
                    HealthStatus::Unhealthy
                },
                message: Some("Database connection status".to_string()),
            }
        })
    }
}

8. Testing

Tideway provides Alba-style testing utilities for easy HTTP endpoint testing:

use tideway::testing::{get, post, TestDb};
use tideway::testing::fake;

#[tokio::test]
async fn test_create_user() {
    let app = create_app();

    let response = post(app, "/api/users")
        .with_json(&serde_json::json!({
            "email": fake::email(),
            "name": fake::name(),
        }))
        .execute()
        .await
        .assert_status(201)
        .assert_json_path("data.email", fake::email());
}

#[tokio::test]
async fn test_with_database() {
    let db = TestDb::new("sqlite::memory:").await.unwrap();

    db.seed("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)").await.unwrap();

    db.with_transaction_rollback(|tx| async move {
        // Test code - transaction will be rolled back
        // Database state is isolated between tests
    }).await.unwrap();
}

9. Development Mode

Enable development mode for enhanced debugging:

use tideway::{ConfigBuilder, DevConfigBuilder};

let config = ConfigBuilder::new()
    .with_dev_config(
        DevConfigBuilder::new()
            .enabled(true)
            .with_stack_traces(true)
            .with_request_dumper(true)
            .build()
    )
    .build()?;  // Returns Result<Config> - validates configuration

Environment Variables:

  • TIDEWAY_DEV_MODE - Enable dev mode (default: false)
  • TIDEWAY_DEV_STACK_TRACES - Include stack traces (default: false)
  • TIDEWAY_DEV_DUMP_REQUESTS - Enable request dumper (default: false)
  • TIDEWAY_DEV_DUMP_PATH - Path pattern to dump (default: all)

10. Logging & Tracing

Structured logging is enabled by default:

#[tokio::main]
async fn main() {
    // Initialize with defaults
    tideway::init_tracing();

    // Or with custom config
    let config = ConfigBuilder::new().build();
    tideway::init_tracing_with_config(&config);

    tracing::info!("Application started");
    tracing::debug!(user_id = 123, "Processing request");
}

All HTTP requests are automatically logged with:

  • Request ID (x-request-id header)
  • Method and URI
  • Response status
  • Response time in milliseconds

Examples

Tideway includes comprehensive examples demonstrating real-world usage:

Complete SaaS Application

examples/saas_app.rs - Full-featured SaaS app with:

  • Database integration
  • JWT authentication
  • Rate limiting
  • CORS configuration
  • OpenAPI documentation
  • Health checks
cargo run --example saas_app --features database,openapi

Custom Database Implementation

examples/custom_database.rs - Implementing a custom DatabasePool:

cargo run --example custom_database --features database

Redis Caching

examples/redis_cache.rs - Using Redis for caching:

cargo run --example redis_cache --features cache-redis

Session Management

examples/sessions_example.rs - Session management examples:

cargo run --example sessions_example --features sessions

Authentication Flow

examples/auth_flow.rs - Complete auth implementation:

  • User registration
  • Login with JWT
  • Protected routes
  • Public endpoints
cargo run --example auth_flow

Testing Guide

examples/testing_example.rs - Testing patterns:

  • Alba-style HTTP testing
  • Database testing with TestDb
  • Error case testing
cargo test --example testing_example

Validation Example

examples/validation_example.rs - Request validation:

  • ValidatedJson extractor
  • ValidatedQuery extractor
  • Custom validators
  • Field-level error handling
cargo run --example validation_example --features validation

Development Mode

examples/dev_mode.rs - Development tools:

  • Enhanced error responses
  • Request/response dumper
  • Stack trace debugging
cargo run --example dev_mode

Production Configuration

examples/production_config.rs - Production setup:

  • Environment-based config
  • Logging setup
  • Graceful shutdown
  • Health monitoring
cargo run --example production_config

WebSocket Chat

examples/websocket_chat.rs - Real-time chat with rooms:

  • WebSocket connection handling
  • Room management
  • Broadcasting messages
  • User join/leave notifications
cargo run --example websocket_chat --features websocket

WebSocket Notifications

examples/websocket_notifications.rs - Real-time notifications:

  • Server-to-client push notifications
  • User-specific channels
  • Integration with background jobs
cargo run --example websocket_notifications --features websocket

Architecture

Tideway follows a layered architecture:

┌─────────────────────────────────┐
│         HTTP Layer              │
│  (Routes, Middleware, Handlers) │
├─────────────────────────────────┤
│       Application Core          │
│   (Business Logic, Services)    │
├─────────────────────────────────┤
│      Infrastructure             │
│ (Database, Cache, External APIs)│
└─────────────────────────────────┘

Key Components:

  • App: Main application structure with routing and middleware
  • AppContext: Dependency injection container for shared state (database, cache, sessions)
  • RouteModule: Trait for modular route organization
  • Config: Environment-aware configuration
  • TidewayError: Unified error handling
  • ApiResponse: Standardized JSON responses

Trait-Based Components:

  • DatabasePool: Abstract database connection pooling (SeaORM, SQLx support)
  • Cache: Key-value caching abstraction (in-memory, Redis)
  • SessionStore: Session management abstraction (in-memory, cookie-based)
  • JobQueue: Background job processing (in-memory, Redis)
  • ConnectionManager: WebSocket connection management (rooms, broadcasting)

Built-in Middleware

All requests automatically include:

  1. Request ID: Unique UUID for request tracking
  2. Body Size Limit: Global default limit (10MB) to prevent DoS attacks
  3. Tracing: Structured logging with request/response details
  4. Error Handling: Automatic error to JSON response conversion
  5. CORS: Configurable CORS support (disabled by default for security)
  6. Rate Limiting: Per-IP and global rate limiting (health endpoints excluded)
  7. Compression: Gzip/Brotli response compression
  8. Security Headers: HSTS, CSP, X-Frame-Options, and more
  9. Timeout: Configurable request timeouts
  10. Request Logging: Structured request/response logging
  11. Metrics: Prometheus metrics collection (optional, uses route templates when available)

Dependency Injection

Tideway provides AppContext for dependency injection:

use tideway::{AppContext, SeaOrmPool, InMemoryCache, InMemorySessionStore};
use std::sync::Arc;

let db_pool = Arc::new(SeaOrmPool::from_config(&db_config).await?);
let cache = Arc::new(InMemoryCache::new(10000));
let sessions = Arc::new(InMemorySessionStore::new(Duration::from_secs(3600)));

let context = AppContext::builder()
    .with_database(db_pool)
    .with_cache(cache)
    .with_sessions(sessions)
    .build();

Use in your handlers:

use axum::extract::State;
use tideway::AppContext;

async fn my_handler(State(ctx): State<AppContext>) -> Json<Response> {
    // Use helper methods for cleaner access
    if let Ok(cache) = ctx.cache() {
        // Use cache - returns error if not configured
    }

    // Or use optional access
    if let Some(cache) = ctx.cache_opt() {
        // Use cache - returns None if not configured
    }

    Json(Response { /* ... */ })
}

Database, Cache & Sessions

Database

Tideway supports multiple database backends through the DatabasePool trait:

  • SeaORM (default): Full-featured ORM with migrations
  • SQLx (coming soon): Async SQL toolkit
use tideway::{SeaOrmPool, DatabasePool};

let pool = SeaOrmPool::from_config(&config).await?;
let pool: Arc<dyn DatabasePool> = Arc::new(pool);

Cache

Multiple cache backends supported:

  • In-Memory: Fast HashMap-based cache (default)
  • Redis: Distributed caching with cache-redis feature
use tideway::cache::{InMemoryCache, RedisCache};
use tideway::CacheExt; // Provides get<T>() and set<T>()

let cache: Arc<dyn Cache> = Arc::new(InMemoryCache::new(10000));

// Type-safe operations
cache.set("user:123", &user_data, Some(Duration::from_secs(3600))).await?;
let user: Option<User> = cache.get("user:123").await?;

Sessions

Session management with multiple storage backends:

  • In-Memory: For development/testing
  • Cookie-Based: Encrypted cookie sessions
use tideway::session::{InMemorySessionStore, CookieSessionStore};
use tideway::{SessionStore, SessionData};

let store: Arc<dyn SessionStore> = Arc::new(
    InMemorySessionStore::new(Duration::from_secs(3600))
);

let mut session = SessionData::new(Duration::from_secs(3600));
session.set("user_id".to_string(), "123".to_string());
store.save("session-id", session).await?;

See docs/database_traits.md, docs/caching.md, and docs/sessions.md for detailed documentation.

Testing

Tideway applications are easy to test, and include Alba-style helpers:

use tideway::testing::{get, post};
use tideway::testing::fake;

#[tokio::test]
async fn test_create_user() {
    let app = create_app();

    post(app, "/api/users")
        .with_json(&serde_json::json!({
            "email": fake::email(),
            "name": fake::name(),
        }))
        .execute()
        .await
        .assert_status(201);
}

See docs/testing.md and examples/testing_example.rs for more patterns.

Run tests:

cargo test

Roadmap

Completed ✅

  • Rate limiting middleware
  • CORS configuration
  • OpenAPI/Swagger generation
  • Request validation support
  • Compression middleware
  • Security headers middleware
  • Request/response logging
  • Timeout middleware
  • Prometheus metrics
  • Global request body size limit (DoS protection)
  • Trait-based database abstraction (SeaORM)
  • Trait-based caching (in-memory, Redis)
  • Trait-based session management (in-memory, cookies)
  • Dependency injection with AppContext
  • Custom validators (UUID, slug, phone, JSON, duration)
  • ValidatedQuery and ValidatedForm extractors
  • Enhanced error handling (context, IDs, stack traces)
  • Alba-style testing utilities
  • Test fixtures and fake data helpers
  • Database testing improvements (seed, reset, transactions)
  • Development mode (enhanced errors, request dumper)
  • Response helpers (paginated, created, no_content)
  • WebSocket support (connection management, rooms, broadcasting)

In Progress 🚧

  • SQLx database backend implementation

Planned 📋

  • CLI tool for scaffolding
  • Deployment guides
  • Additional cache backends (Memcached)
  • Additional session backends (database-backed)

Performance

Tideway adds minimal overhead compared to raw Axum. Benchmarks are available in the benches/ directory.

Run benchmarks:

cargo bench

See benches/README.md for detailed performance metrics.

Contributing

Contributions are welcome! This is currently in early development.

License

MIT

Acknowledgments

Built with:


Start building your SaaS with Tideway today!