Files
Htmx/README.md
T
shaamilahmed 5f0c6bed91
Production Deployment / Build and Push Docker Image (push) Failing after 11m59s
Production Deployment / Deploy to DigitalOcean Droplet (push) Has been skipped
Production Deployment / Deploy to Google Cloud Run (push) Has been skipped
feat: add infrastructure dockerfile, droplet documentation, and gitea deployment workflow
2026-05-31 06:51:18 +05:00

10 KiB

Stick

Stick is a high-fidelity Rust web application starter template built on Axum, Askama, and MongoDB.

Unlike traditional MVC frameworks that segment code horizontally by technical layers (controllers, models, views), Stick is organized by vertical slices. Each domain feature—such as authentication, tasks, or audit logging—lives together in its own self-contained module. This simplifies code maintenance and cognitive load in medium-to-large codebases where horizontal layers inevitably grow difficult to navigate.


Architectural Philosophy & Layout

1. Vertical Slice Domain Layout

Every domain feature owns its database access models, business logic, endpoints, and markup templates. For example:

  • src/auth/models.rs: BSON data structures and claims.
  • src/auth/repository.rs: Database operations and query logic.
  • src/auth/handlers.rs: Request/response lifecycle logic.
  • src/auth/extractors.rs: Axum extractors for user session handling.
  • templates/auth/: HTML templates compiled at build-time.

2. Separating Components from Documentation

To prevent domain pollution and maintain code integrity, we strictly separate template assets:

  • Reusable UI Components (templates/components/): Reserved strictly for operational UI blocks used across the application. Reusable elements like fields, dropdowns, calendars, and buttons are defined as Askama macros inside templates/components/macros.html.
  • System Documentation (templates/docs/ & src/docs/): Design manuals, guidelines, and interactive preview sandboxes are isolated. The Documentation portal is served on a dedicated /docs route, separate from core application logic.

3. Key Stack Decisions

  • Axum (v0.8): Modern, async-first routing and middleware.
  • Askama (v0.16): Evaluates and compiles HTML templates into Rust code at compile time. Syntax, variable existence, and type constraints are verified by the compiler.
  • MongoDB: Standard Rust driver configured with BSON serialization.
  • Tailwind CSS: Utility-first styling compiled using a Node-based wrapper process.
  • JWT Authentication: Managed via JSON Web Tokens stored in secure, encrypted HttpOnly cookies.

Audit Logging Framework (First-Class Feature)

Stick contains a built-in, request-scoped Audit Logging and Replay framework designed to make all critical system mutations (Create, Read, Update, Delete, Search) auditable.

       Request
          │
          ▼
┌──────────────────┐
│   AuditLogger    │ ──► Resolves: DB, User, User-Agent
│    Extractor     │
└──────────────────┘
          │
          ├──► Parses "x-forwarded-for" & "x-real-ip" proxy headers
          ├──► Parses Direct ConnectInfo TCP socket IP
          │
          ▼
┌──────────────────┐
│  Log Entry Writ  │ ──► Formats: "ProxyIP (Socket: SocketIP)" if mismatched
└──────────────────┘
          │
          ▼
┌──────────────────┐
│  MongoDB Store   │ ──► Stores structured entry + JSON state snapshot
└──────────────────┘

1. Request-Scoped Extractor (AuditLogger)

The custom AuditLogger extractor implements Axum's FromRequestParts trait. When added to any handler function signature, it automatically resolves:

  • Database connection reference.
  • Optional authenticated user context.
  • Client metadata (IP address and User-Agent).

2. Intelligent IP & Proxy Resolution

To support load-balanced or proxied environments, the extractor resolves and logs both network paths:

  • Inspects standard proxy headers (x-forwarded-for, x-real-ip).
  • Captures the direct TCP client connection address via Axum's ConnectInfo.
  • If the proxy IP and connecting socket IP differ, the logger combines them (e.g. 10.0.0.1 (Socket: 127.0.0.1)) ensuring full visibility of both the request origin and proxy hop.

3. Historical State Snapshotting (Replayability)

Log entries include a serialized JSON payload field containing the state of the affected entity. For edits, it saves the state transition. For deletions, it saves the final snapshot before removal, ensuring data remains reconstructible in the event of an audit inquiry.

4. Admin Audit Panel

Authorized administrators can review, search, and audit system activities at /auth/audit. The dashboard allows filtering entries by:

  • Username: Filter logs to a specific actor.
  • Event Type: Filter by actions (e.g. Login, Create, Update, Delete).
  • Entity Type & ID: Pinpoint history for a specific resource (e.g. Task, Developer).
  • Timeline: Filter logs by start and end timestamps.

Core Features Included

Self-Provisioning Administrator

On startup, if the MongoDB users collection is empty, Stick automatically provisions an administrator account with username admin, generates a secure random 16-character alphanumeric password, and outputs the credentials directly to the console logs:

======================================================
CREATED INITIAL ADMINISTRATOR ACCOUNT:
Username: admin
Password: [GeneratedPassword]
======================================================

User Management Panel (Admin Only)

Accessible at /auth/users. Administrators can view all users, register new ones, toggle administrative roles, reset passwords, or delete standard accounts (with safety blocks preventing administrators from deleting their own active profile or revoking their own privileges).

Self-Service Password Reset

Any logged-in user can change their password at /auth/password by clicking their username in the navigation bar.


Setup and Running

Prerequisites

  • Rust: Toolchain v1.75+ (for native async traits).
  • Node.js & npm: Required to build Tailwind CSS.
  • MongoDB: Running locally on mongodb://127.0.0.1:27017.

Local Setup

  1. Copy the environment configuration:
    cp .env.example .env
    
  2. Build Tailwind CSS styling:
    npm install
    npx tailwindcss -i src/input.css -o static/tailwind.css
    
  3. Run the development server:
    cargo run
    
    The server will start listening at http://127.0.0.1:3009 (as configured in .env).

Running with Docker

A multi-stage Dockerfile compiles Tailwind, compiles the Rust binary, and bundle a lightweight Debian runtime container.

  1. Build the image:
    docker build -t stick .
    
  2. Start the container:
    docker run --name stick-app --rm --network="host" \
      -e DATABASE_URL="mongodb://127.0.0.1:27017" \
      -e DATABASE_NAME="stick_db" \
      -e JWT_SECRET="super_secret_template_signing_key_that_is_at_least_32_characters_long" \
      -e HOST="127.0.0.1" \
      -e PORT="3009" \
      stick
    

Developer Cookbook

1. Adding a Feature Slice (e.g. projects)

  1. Create a module folder: src/projects/.
  2. Define models in models.rs and database access operations in repository.rs.
  3. Add request handlers in handlers.rs.
  4. Create src/projects/mod.rs to expose the router:
    pub fn router<S>() -> Router<S>
    where
        crate::common::config::Config: axum::extract::FromRef<S>,
        mongodb::Database: axum::extract::FromRef<S>,
        S: Clone + Send + Sync + 'static,
    {
        Router::new()
            .route("/projects", get(handlers::get_projects))
    }
    
  5. Place HTML templates under templates/projects/ extending the base.html layout.
  6. Register the module in src/main.rs and merge the sub-router:
    let app = Router::new()
        .merge(main_view::router())
        .merge(auth::router())
        .merge(projects::router()) // Merge domain router
        .with_state(state);
    

2. Logging Operations with AuditLogger

To log any action in a handler, inject logger: AuditLogger and invoke .log() after a successful DB mutation:

use crate::audit::AuditLogger;
use serde_json::json;

pub async fn delete_task_handler(
    State(repo): State<MongoTaskRepository>,
    logger: AuditLogger, // <-- Automatically injected
    Path(task_id): Path<ObjectId>,
) -> Result<impl IntoResponse, AppError> {
    // 1. Fetch task and perform mutation
    let task = repo.get_by_id(&task_id).await?;
    repo.delete(&task_id).await?;

    // 2. Audit the event (single line, fully context-aware)
    logger.log(
        "Delete",                   // Event action
        "Task",                     // Entity type
        Some(task_id),              // Target entity ID
        Some(format!("Deleted task '{}'", task.title)), // Description
        Some(json!(task)),          // Serialized payload for replayability
    ).await;

    Ok(Redirect::to("/tasks"))
}

Production Deployment to a Cloud Host (DigitalOcean Droplet)

For production deployments (such as to a DigitalOcean Droplet), we avoid using --network="host". Instead, we deploy both the database and the application container to a shared, user-defined Docker bridge network named dockernet. This provides secure internal DNS resolution and container isolation.

1. Create the Isolated Docker Network

On your Droplet, create the bridge network:

docker network create dockernet

2. Build and Run the Database Infrastructure

Build the custom MongoDB infrastructure image using the dedicated Infra.DockerFile:

# 1. Build the database image
docker build -t stick-db -f Infra.DockerFile .

# 2. Run the database container on 'dockernet' with host persistence
docker run --name stick-mongodb \
  --network dockernet \
  -v /var/lib/mongodb/data:/data/db \
  -d \
  stick-db

Note: The database container is named stick-mongodb. Other containers on dockernet can now resolve this container using mongodb://stick-mongodb:27017.

3. Build and Deploy the Application Container

Build the main application image and launch it on the same network:

# 1. Build the application image
docker build -t stick-app .

# 2. Run the application container, linking to the database using its container name
docker run --name stick-app-container \
  --network dockernet \
  -p 80:3007 \
  -e DATABASE_URL="mongodb://stick-mongodb:27017" \
  -e DATABASE_NAME="stick_db" \
  -e JWT_SECRET="your_secure_production_jwt_signing_key_at_least_32_chars_long" \
  -e HOST="0.0.0.0" \
  -e PORT="3007" \
  -d \
  stick-app

Note: -p 80:3007 maps the Droplet's external HTTP port 80 to the application's internal container port 3007.