diff --git a/Cargo.lock b/Cargo.lock
index 7ba0b3c..0a636e4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2743,9 +2743,11 @@ dependencies = [
  "ordered-float",
  "png",
  "reqwest",
+ "serde",
  "thiserror 1.0.64",
  "tikv-jemallocator",
  "tokio",
+ "toml",
  "tracing",
  "tracing-subscriber",
  "wxbox-grib2",
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..6941b58
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,16 @@
+[sources.grib2.noaa_mrms_merged_composite_reflectivity_qc]
+from = "https://mrms.ncep.noaa.gov/data/2D/HAWAII/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz"
+needs_gzip = true
+valid_for = 120
+palette = """
+Color:  10   164  164  255    100 100 192
+Color:  20    64  128  255     32  64 128
+Color:  30     0  255    0      0 128   0
+Color:  40   255  255    0    255 128   0
+Color:  50   255    0    0    160   0   0
+Color:  60   255    0  255    128   0 128
+Color:  70   255  255  255    128 128 128
+Color:  80   128  128  128
+"""
+missing = -99.0
+no_coverage = -999.0
\ No newline at end of file
diff --git a/wxbox-tiler/Cargo.toml b/wxbox-tiler/Cargo.toml
index cf3b6b3..eb71b27 100644
--- a/wxbox-tiler/Cargo.toml
+++ b/wxbox-tiler/Cargo.toml
@@ -19,6 +19,8 @@ mime = "0.3.17"
 wxbox-grib2 = { version = "0.1", path = "../wxbox-grib2" }
 tracing = "0.1"
 tracing-subscriber = "0.3"
+toml = "0.8"
+serde = { version = "1", features = ["derive"] }
 
 [dev-dependencies]
 approx = "0.5"
diff --git a/wxbox-tiler/src/config.rs b/wxbox-tiler/src/config.rs
new file mode 100644
index 0000000..978a7bc
--- /dev/null
+++ b/wxbox-tiler/src/config.rs
@@ -0,0 +1,23 @@
+use std::collections::HashMap;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct Config {
+    pub sources: Sources
+}
+
+#[derive(Deserialize)]
+pub struct Sources {
+    pub grib2: HashMap<String, Grib2Source>
+}
+
+#[derive(Deserialize)]
+pub struct Grib2Source {
+    pub from: String,
+    pub needs_gzip: bool,
+    pub valid_for: u64,
+    pub palette: String,
+    pub missing: Option<f64>,
+    pub range_folded: Option<f64>,
+    pub no_coverage: Option<f64>
+}
\ No newline at end of file
diff --git a/wxbox-tiler/src/main.rs b/wxbox-tiler/src/main.rs
index 7caa532..4a2daf0 100644
--- a/wxbox-tiler/src/main.rs
+++ b/wxbox-tiler/src/main.rs
@@ -1,32 +1,42 @@
 pub(crate) mod sources;
 
 mod pixmap;
+mod config;
 
 use std::collections::{BTreeMap, HashMap};
+use std::env::args;
 use std::fmt::Debug;
+use std::fs;
 use std::sync::Arc;
 use tokio::sync::RwLock;
 use std::time::SystemTime;
 use actix_web::{App, HttpServer};
 use actix_web::web::Data;
 use wxbox_grib2::GribMessage;
+use crate::config::Config;
 
 pub struct AppState {
     grib2_cache: RwLock<HashMap<String, Arc<RwLock<GribMessage>>>>,
-    grib2_cache_timestamps: RwLock<HashMap<String, SystemTime>>
+    grib2_cache_timestamps: RwLock<HashMap<String, SystemTime>>,
+    config: Config
 }
 
 #[actix_web::main]
 async fn main() -> std::io::Result<()> {
     tracing_subscriber::fmt::init();
 
+    let config_path = args().nth(1).unwrap();
+    let config_str = fs::read_to_string(config_path).unwrap();
+    let config: Config = toml::from_str(&config_str).unwrap();
+
     let data = Data::new(AppState {
         grib2_cache: RwLock::new(HashMap::new()),
-        grib2_cache_timestamps: RwLock::new(HashMap::new())
+        grib2_cache_timestamps: RwLock::new(HashMap::new()),
+        config
     });
     HttpServer::new(move || {
         App::new()
-            .service(sources::noaa::noaa_mrms_merged_composite_reflectivity_qc)
+            .service(sources::grib2::grib2_source)
             .app_data(data.clone())
     })
         .bind(("::", 8080))?
diff --git a/wxbox-tiler/src/sources/grib2.rs b/wxbox-tiler/src/sources/grib2.rs
index b9bf6c4..dab71a9 100644
--- a/wxbox-tiler/src/sources/grib2.rs
+++ b/wxbox-tiler/src/sources/grib2.rs
@@ -3,12 +3,18 @@ use std::f64::consts::PI;
 use std::io::{BufWriter, Cursor, Read};
 use std::sync::Arc;
 use std::time::SystemTime;
+use actix_web::error::UrlencodedError::ContentType;
+use actix_web::http::StatusCode;
+use actix_web::HttpResponse;
+use actix_web::web::Data;
 use flate2::read::GzDecoder;
 use png::{BitDepth, ColorType, Encoder};
 use tokio::sync::RwLock;
 use wxbox_grib2::GribMessage;
 use wxbox_grib2::wgs84::LatLong;
 use wxbox_pal::{Color, ColorPalette, Palette};
+use crate::AppState;
+use crate::config::Grib2Source;
 use crate::pixmap::Pixmap;
 
 pub async fn needs_reload(lct: &RwLock<HashMap<String, SystemTime>>, lutkey: &String, valid_for: u64) -> bool {
@@ -125,6 +131,35 @@ pub async fn render(xtile: f64, ytile: f64, z: i32, tilesize: usize, pal: Palett
     buf
 }
 
+#[actix_web::get("/grib2/{id}/{z}/{x}/{y}.png")]
+pub async fn grib2_source(path: actix_web::web::Path<(String, i32, u32, u32)>, data: Data<AppState>) -> HttpResponse {
+    if let Some(known_source) = data.config.sources.grib2.get(&path.0) {
+        reload_if_required(
+            &known_source.from,
+            known_source.needs_gzip,
+            known_source.valid_for.into(),
+            &path.0,
+            &data.grib2_cache_timestamps,
+            &data.grib2_cache
+        ).await;
+        let lct_reader = data.grib2_cache_timestamps.read().await;
+        if let Some(grib2) = data.grib2_cache.read().await.get(&path.0) {
+            HttpResponse::Ok()
+                .insert_header(actix_web::http::header::ContentType(mime::IMAGE_PNG))
+                // TODO: use the timestamp in the grib2 ID section
+                .insert_header(("x-wxbox-tiler-data-valid-time", lct_reader.get(&path.0).expect("impossible").duration_since(::std::time::UNIX_EPOCH).expect("time went backwards").as_secs().to_string()))
+                .insert_header(("Access-Control-Allow-Origin", "*"))
+                .insert_header(("Access-Control-Expose-Headers", "*"))
+                .insert_header(("Access-Control-Allow-Headers", "*"))
+                .body(crate::sources::grib2::render(path.2 as f64, path.3 as f64, path.1, 256, wxbox_pal::parser::parse(&known_source.palette).unwrap(), grib2, known_source.missing, known_source.range_folded, known_source.no_coverage).await)
+        } else {
+            HttpResponse::new(StatusCode::NOT_FOUND)
+        }
+    } else {
+        HttpResponse::new(StatusCode::NOT_FOUND)
+    }
+}
+/*
 #[macro_export]
 macro_rules! grib2_handler {
     (mount $f:ident, at: $path:expr, from: $from:expr, needs_gzip: $needs_gzip:expr, valid_for: $valid_for:expr, palette: $pal:expr, missing: $missing:expr, range_folded: $rf:expr, no_coverage: $nc:expr) => {
@@ -152,4 +187,4 @@ macro_rules! grib2_handler {
             }
         }
     };
-}
\ No newline at end of file
+}*/
\ No newline at end of file
diff --git a/wxbox-tiler/src/sources/noaa/mod.rs b/wxbox-tiler/src/sources/noaa/mod.rs
index 4f2be63..31b1c45 100644
--- a/wxbox-tiler/src/sources/noaa/mod.rs
+++ b/wxbox-tiler/src/sources/noaa/mod.rs
@@ -1,13 +1,13 @@
-use crate::grib2_handler;
-
+//use crate::grib2_handler;
+/*
 grib2_handler! {
     mount noaa_mrms_merged_composite_reflectivity_qc,
     at: "/noaa_mrms_merged_composite_reflectivity_qc/{z}/{x}/{y}.png",
-    from: "https://mrms.ncep.noaa.gov/data/2D/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz",
+    from: "https://mrms.ncep.noaa.gov/data/2D/HAWAII/MergedReflectivityQCComposite/MRMS_MergedReflectivityQCComposite.latest.grib2.gz",
     needs_gzip: true,
     valid_for: 120,
     palette: wxbox_pal::parser::parse(wxbox_pal::default_palettes::DEFAULT_REFLECTIVITY_PALETTE).unwrap(),
     missing: Some(-99.0),
     range_folded: None,
     no_coverage: Some(-999.0)
-}
\ No newline at end of file
+}*/
\ No newline at end of file