Push notifications from Rust
Rust programs tend to be fast and silent. Here’s how to make yours speak up when something happens.
The simplest version: ureq (sync)
# Cargo.toml
[dependencies]
ureq = "2"fn notify(title: &str) {
let key = match std::env::var("TRIGGER_FYI_SECRET_KEY") {
Ok(k) => k,
Err(_) => return,
};
let _ = ureq::post(&format!("https://trigger.fyi/{}", key))
.set("Content-Type", "text/plain")
.timeout(std::time::Duration::from_secs(3))
.send_string(title);
// Errors are ignored — fire-and-forget
}
fn main() {
// ... your work ...
notify("Job complete");
}With metadata: reqwest (async)
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"use std::collections::HashMap;
use std::env;
#[derive(serde::Serialize)]
struct Payload {
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
level: Option<String>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
meta: HashMap<String, String>,
}
async fn fyi(title: &str, body: Option<&str>, level: Option<&str>, meta: HashMap<String, String>) {
let key = match env::var("TRIGGER_FYI_SECRET_KEY") {
Ok(k) => k,
Err(_) => return,
};
let payload = Payload {
title: title.to_string(),
body: body.map(String::from),
level: level.map(String::from),
meta,
};
let client = reqwest::Client::new();
let _ = client
.post(format!("https://trigger.fyi/{}", key))
.timeout(std::time::Duration::from_secs(3))
.json(&payload)
.send()
.await;
}
// Convenience wrappers
async fn fyi_simple(title: &str) {
fyi(title, None, None, HashMap::new()).await;
}
async fn fyi_critical(title: &str, body: &str) {
fyi(title, Some(body), Some("critical"), HashMap::new()).await;
}
async fn fyi_log(title: &str) {
fyi(title, None, Some("log"), HashMap::new()).await;
}In a CLI tool
use std::time::Instant;
#[tokio::main]
async fn main() {
let start = Instant::now();
match run_backup().await {
Ok(count) => {
let elapsed = start.elapsed().as_secs();
fyi_simple(&format!("Backup complete: {} items in {}s", count, elapsed)).await;
}
Err(e) => {
fyi_critical("Backup failed", &e.to_string()).await;
std::process::exit(1);
}
}
}In an async service
use axum::{routing::post, Router, Json, extract::State};
use std::sync::Arc;
struct AppState {
// your state
}
async fn signup_handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<serde_json::Value>,
) -> Json<serde_json::Value> {
let user = create_user(&state, &payload).await.unwrap();
// Spawn notification — doesn't block the response
let email = user.email.clone();
tokio::spawn(async move {
fyi_simple(&format!("New signup: {}", email)).await;
});
Json(serde_json::json!({ "id": user.id, "email": user.email }))
}tokio::spawn fires the notification in a separate task. The handler returns immediately. The task runs in the background — cleanup is handled by the runtime when it completes.
In a long-running job
use std::time::Instant;
async fn process_dataset(dataset_id: u64, total: usize) {
let start = Instant::now();
let mut processed = 0;
for batch in get_batches(dataset_id).await {
process_batch(&batch).await;
processed += batch.len();
// Log progress at 25%, 50%, 75%
let pct = (processed * 100) / total;
if pct % 25 == 0 && pct > 0 {
fyi_log(&format!("Progress: {}% ({}/{})", pct, processed, total)).await;
}
}
let elapsed = start.elapsed().as_secs();
fyi_simple(&format!("Dataset complete: {} records in {}s", total, elapsed)).await;
}Setup
npx trigger.fyiGenerates a key, subscribes your device. In your environment:
export TRIGGER_FYI_SECRET_KEY=your_key_hereOr set it in your deployment environment. The key is read at call time via env::var — no global initialization.
Related: Push notifications from Go · Push notifications from bash · Cron job notifications · What is trigger.fyi?