Skip to main content

postrust_server/
custom.rs

1//! The built-in routes under `/_`: health and readiness.
2//!
3//! Routes of your own do not belong here. A binary of yours nests them with
4//! [`crate::Builder::nest`], at a prefix of its own or beneath `/_`, without
5//! forking this crate; see docs/custom-routes.md. The handlers at the bottom of
6//! this file are sketches of what such routes look like.
7
8use axum::{
9    extract::State,
10    http::StatusCode,
11    response::{IntoResponse, Json},
12    routing::get,
13    Router,
14};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17
18use crate::state::AppState;
19
20/// Build the custom routes router.
21pub fn custom_router() -> Router<Arc<AppState>> {
22    Router::new()
23        .route("/health", get(health_check))
24        .route("/ready", get(readiness_check))
25}
26
27/// Build the router served on `admin_server_port`.
28///
29/// Liveness and readiness only. The point of a separate port is that it can be
30/// reachable from an orchestrator without the API being reachable from the same
31/// place, so nothing that reads or writes data belongs here.
32///
33/// `/live` and `/ready` are the names PostgREST's admin server uses; `/health`
34/// is the same answer as `/live` under the name the main API already serves.
35///
36/// With the `otel` feature, `/metrics` too: the server's metrics as
37/// Prometheus text. Here and never on the API's port, for the same reason as
38/// the rest -- what the pool and the token cache are doing is the operator's
39/// business, not every client's.
40pub fn admin_router() -> Router<Arc<AppState>> {
41    let router = Router::new()
42        .route("/live", get(health_check))
43        .route("/health", get(health_check))
44        .route("/ready", get(readiness_check));
45    #[cfg(feature = "otel")]
46    let router = router.route("/metrics", get(crate::otel::metrics_handler));
47    router
48}
49
50// =============================================================================
51// Health & Readiness
52// =============================================================================
53
54async fn health_check() -> Json<HealthResponse> {
55    Json(HealthResponse {
56        status: "ok".to_string(),
57        version: env!("CARGO_PKG_VERSION").to_string(),
58    })
59}
60
61async fn readiness_check(State(state): State<Arc<AppState>>) -> impl IntoResponse {
62    // Keys from `PGRST_JWT_JWKS_URL` that have never loaded mean every token
63    // they would verify is refused: a replica in that state should not be
64    // sent traffic, though it is alive and will recover by itself.
65    let jwks = state.jwt_config.jwks.as_ref().map(|jwks| jwks.status());
66    let jwks_loaded = jwks.as_ref().map(|status| status.loaded);
67    let jwks_problem = jwks.filter(|status| !status.loaded).map(|status| {
68        format!(
69            "the keys at PGRST_JWT_JWKS_URL have not loaded: {}",
70            status.last_error.as_deref().unwrap_or("not fetched yet")
71        )
72    });
73
74    // Check database connectivity
75    let (database, message) = match sqlx::query("SELECT 1").execute(&state.pool).await {
76        Ok(_) => (true, jwks_problem),
77        Err(e) => (false, Some(e.to_string())),
78    };
79    let ready = database && jwks_loaded != Some(false);
80    (
81        if ready {
82            StatusCode::OK
83        } else {
84            StatusCode::SERVICE_UNAVAILABLE
85        },
86        Json(ReadinessResponse {
87            ready,
88            database,
89            jwks: jwks_loaded,
90            message,
91        }),
92    )
93}
94
95// =============================================================================
96// Response Types
97// =============================================================================
98
99#[derive(Serialize)]
100struct HealthResponse {
101    status: String,
102    version: String,
103}
104
105#[derive(Serialize)]
106struct ReadinessResponse {
107    ready: bool,
108    database: bool,
109    /// Whether the keys at `PGRST_JWT_JWKS_URL` have loaded; absent when it
110    /// is not set.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    jwks: Option<bool>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    message: Option<String>,
115}
116
117// =============================================================================
118// Example: Webhook Handler
119// =============================================================================
120
121#[derive(Deserialize)]
122#[allow(dead_code)]
123struct WebhookPayload {
124    event_type: String,
125    data: serde_json::Value,
126}
127
128#[allow(dead_code)]
129async fn handle_webhook(
130    State(state): State<Arc<AppState>>,
131    Json(payload): Json<WebhookPayload>,
132) -> impl IntoResponse {
133    tracing::info!("Received webhook: {}", payload.event_type);
134
135    // Process webhook with database access
136    let _pool = &state.pool;
137
138    // Your custom logic here...
139
140    (
141        StatusCode::OK,
142        Json(serde_json::json!({ "received": true })),
143    )
144}
145
146// =============================================================================
147// Example: Custom RPC that bypasses PostgREST
148// =============================================================================
149
150#[derive(Deserialize)]
151#[allow(dead_code)]
152struct CustomRpcRequest {
153    action: String,
154    params: serde_json::Value,
155}
156
157#[allow(dead_code)]
158async fn custom_rpc(
159    State(state): State<Arc<AppState>>,
160    Json(req): Json<CustomRpcRequest>,
161) -> impl IntoResponse {
162    // Direct database access for custom operations
163    let result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users")
164        .fetch_one(&state.pool)
165        .await;
166
167    match result {
168        Ok(count) => (
169            StatusCode::OK,
170            Json(serde_json::json!({
171                "action": req.action,
172                "result": count
173            })),
174        ),
175        Err(e) => (
176            StatusCode::INTERNAL_SERVER_ERROR,
177            Json(serde_json::json!({
178                "error": e.to_string()
179            })),
180        ),
181    }
182}