CJA

Cron, Jobs, and Axum

A Rust web framework that handles the boring-but-critical infrastructure so you can focus on your app.

#[derive(Clone)]
struct App { db: PgPool, cookie_key: CookieKey }

impl AppState for App {
    fn db(&self) -> &PgPool { &self.db }
    fn cookie_key(&self) -> &CookieKey { &self.cookie_key }
}

async fn run() -> Result<()> {
    let app = App::from_env().await?;
    let tasks = vec![
        NamedTask::spawn("server", run_server(routes(app.clone()))),
        NamedTask::spawn("jobs", job_worker(app, Jobs)),
    ];
    wait_for_first_error(tasks).await
}

What's in the box

Background Jobs

Database-backed persistence with retry and exponential backoff. Dead letter queue for failed jobs. Cancellation tokens for graceful shutdown.

#[derive(Debug, Serialize, Deserialize, Clone)]
struct SendEmail { to: String }

#[async_trait]
impl Job<AppState> for SendEmail {
    const NAME: &'static str = "SendEmail";
    async fn run(&self, state: AppState) -> Result<()> {
        // your logic here
        Ok(())
    }
}

Cron Scheduling

Interval or cron expression scheduling with timezone awareness. Distributed locking ensures single execution across instances.

let mut registry = CronRegistry::new();

registry.register_job(
    CleanupJob,
    Some("Remove expired sessions"),
    Duration::from_secs(3600),
);

registry.register_with_cron(
    "daily-report", None,
    "0 0 9 * * *",  // 9am daily
    |state, _| Box::pin(async move {
        generate_report(state).await
    }),
)?;

Production Server

Axum-based HTTP server with cookie-based DB-backed sessions, structured tracing with OpenTelemetry and Sentry, and zero-downtime reloading.

fn routes(state: AppState) -> Router {
    Router::new()
        .route("/", get(home))
        .route("/api/items", post(create_item))
        .with_state(state)
}
// Sessions work as extractors:
async fn home(
    Session(session): Session<MySession>,
) -> impl IntoResponse { /* ... */ }

A complete app in 30 lines

use cja::app_state::AppState;
use cja::server::cookies::CookieKey;
use cja::jobs::Job;

#[derive(Clone)]
struct App { db: PgPool, cookie_key: CookieKey }

impl AppState for App {
    fn version(&self) -> &str { env!("CARGO_PKG_VERSION") }
    fn db(&self) -> &PgPool { &self.db }
    fn cookie_key(&self) -> &CookieKey { &self.cookie_key }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Digest;

#[async_trait]
impl Job<App> for Digest {
    const NAME: &'static str = "Digest";
    async fn run(&self, app: App) -> Result<()> {
        // send the daily digest
        Ok(())
    }
}

fn routes(app: App) -> Router {
    Router::new()
        .route("/", get(|| async { "Hello from CJA" }))
        .with_state(app)
}

Get started

1

Add CJA to your project

cargo add cja
2

Set up your database

export DATABASE_URL=postgres://localhost/myapp
3

Run it

cargo run

Requires PostgreSQL. See the full documentation for a complete guide.