cja/lib.rs
1#![doc(html_favicon_url = "https://cja.app/favicon.svg")]
2//! # CJA — Cron, Jobs and Axum
3//!
4//! > **ALPHA**: CJA is under active development. APIs may change between releases.
5//!
6//! A Rust web framework that combines background job processing, cron scheduling,
7//! and an HTTP server built on [Axum](https://docs.rs/axum).
8//!
9//! ## Overview
10//!
11//! CJA provides three integrated subsystems:
12//!
13//! - **[`jobs`]** — PostgreSQL-backed background job queue with automatic retries,
14//! exponential backoff, dead letter queue, and graceful shutdown
15//! - **[`cron`]** — Scheduled task execution with both interval-based and cron expression
16//! scheduling, timezone support
17//! - **[`server`]** — HTTP server on Axum with encrypted cookie management, database-backed
18//! sessions, and zero-downtime reload via systemfd
19//!
20//! All three share a single [`app_state::AppState`] and coordinate shutdown via
21//! [`tokio_util::sync::CancellationToken`].
22//!
23//! ## Getting Started
24//!
25//! Add CJA to your project:
26//!
27//! ```bash
28//! cargo add cja
29//! ```
30//!
31//! ### 1. Implement `AppState`
32//!
33//! ```rust
34//! use cja::app_state::AppState;
35//! use cja::server::cookies::CookieKey;
36//!
37//! #[derive(Clone)]
38//! struct MyAppState {
39//! db: sqlx::PgPool,
40//! cookie_key: CookieKey,
41//! }
42//!
43//! impl AppState for MyAppState {
44//! fn version(&self) -> &str { "1.0.0" }
45//! fn db(&self) -> &sqlx::PgPool { &self.db }
46//! fn cookie_key(&self) -> &CookieKey { &self.cookie_key }
47//! }
48//! ```
49//!
50//! [`app_state::AppState`] is cloned per request — wrap expensive resources in `Arc`.
51//!
52//! ### 2. Set Up the Database
53//!
54//! CJA requires `PostgreSQL`. Set `DATABASE_URL` and run migrations on startup:
55//!
56//! ```rust,ignore
57//! let pool = sqlx::postgres::PgPoolOptions::new()
58//! .max_connections(5)
59//! .connect(&std::env::var("DATABASE_URL")?)
60//! .await?;
61//!
62//! // Run CJA's built-in migrations (jobs, crons, sessions, dead_letter_jobs)
63//! cja::db::run_migrations(&pool).await?;
64//! ```
65//!
66//! ### 3. Wire Up `main()`
67//!
68//! ```rust,ignore
69//! use cja::setup::{setup_sentry, setup_tracing};
70//! use cja::tasks::NamedTask;
71//! use cja::jobs::CancellationToken;
72//!
73//! fn main() -> cja::Result<()> {
74//! let _sentry_guard = setup_sentry();
75//! tokio::runtime::Builder::new_multi_thread()
76//! .enable_all()
77//! .build()?
78//! .block_on(run())
79//! }
80//!
81//! async fn run() -> cja::Result<()> {
82//! // Keep the Eyes handle alive for graceful shutdown
83//! let _eyes_handle = setup_tracing("my-app")?;
84//!
85//! let app_state = MyAppState { /* ... */ };
86//! let shutdown_token = CancellationToken::new();
87//!
88//! let mut tasks = vec![];
89//!
90//! // HTTP server
91//! tasks.push(NamedTask::spawn("server",
92//! cja::server::run_server(routes(app_state.clone()))
93//! ));
94//!
95//! // Job worker
96//! tasks.push(NamedTask::spawn("jobs",
97//! cja::jobs::worker::job_worker(
98//! app_state.clone(),
99//! Jobs, // your job registry (see jobs module docs)
100//! std::time::Duration::from_secs(60),
101//! cja::jobs::DEFAULT_MAX_RETRIES,
102//! shutdown_token.clone(),
103//! cja::jobs::DEFAULT_LOCK_TIMEOUT,
104//! )
105//! ));
106//!
107//! // Signal handler
108//! let token = shutdown_token.clone();
109//! tasks.push(NamedTask::spawn("signals", async move {
110//! tokio::signal::ctrl_c().await?;
111//! token.cancel();
112//! Ok(())
113//! }));
114//!
115//! cja::tasks::wait_for_first_error(tasks).await
116//! }
117//! ```
118//!
119//! See `crates/cja.app/` in the repository for a complete working example.
120//!
121//! ## Configuration
122//!
123//! | Variable | Required | Default | Description |
124//! |----------|----------|---------|-------------|
125//! | `DATABASE_URL` | Yes | — | `PostgreSQL` connection string |
126//! | `PORT` | No | `3000` | HTTP server port |
127//! | `COOKIE_KEY` | No | Auto-generated | Base64-encoded 64-byte cookie encryption key. **Must be set in production** for session persistence across restarts. |
128//! | `SERVER_DISABLED` | No | `false` | Set to `"true"` to skip starting the HTTP server |
129//! | `JOBS_DISABLED` | No | `false` | Set to `"true"` to skip starting the job worker |
130//! | `CRON_DISABLED` | No | `false` | Set to `"true"` to skip starting the cron worker |
131//! | `RUST_LOG` | No | `info` | Tracing filter directive |
132//! | `JSON_LOGS` | No | — | If set, outputs JSON logs instead of tree format |
133//! | `SENTRY_DSN` | No | — | Enables Sentry error tracking |
134//! | `HONEYCOMB_API_KEY` | No | — | Enables OpenTelemetry export to Honeycomb |
135//! | `EYES_ORG_ID` | No | — | Eyes tracing org ID (both `ORG_ID` and `APP_ID` required) |
136//! | `EYES_APP_ID` | No | — | Eyes tracing app ID |
137//! | `EYES_URL` | No | `https://eyes.coreyja.com` | Eyes server URL |
138//!
139//! ## Feature Flags
140//!
141//! ```toml
142//! [features]
143//! default = ["cron", "jobs"]
144//! cron = ["jobs"] # cron depends on jobs
145//! jobs = []
146//! testing = [] # enables mock OAuth for tests
147//! ```
148//!
149//! Disabling `jobs` at compile time also disables `cron`.
150//!
151//! ## Re-exports
152//!
153//! CJA re-exports key dependencies so you don't need to add them directly:
154//!
155//! ```rust
156//! use cja::sqlx;
157//! use cja::uuid;
158//! use cja::color_eyre;
159//! use cja::maud;
160//! use cja::chrono;
161//! use cja::chrono_tz;
162//! ```
163
164pub use sqlx;
165pub use uuid;
166
167#[cfg(feature = "cron")]
168pub mod cron;
169#[cfg(feature = "jobs")]
170pub mod jobs;
171pub mod server;
172#[cfg(feature = "testing")]
173pub mod testing;
174
175pub mod app_state;
176pub mod setup;
177
178pub use color_eyre;
179pub use color_eyre::Result;
180
181pub use maud;
182
183pub mod db;
184pub mod tasks;
185
186pub use chrono;
187pub use chrono_tz;