diff --git a/Cargo.lock b/Cargo.lock
index d9af38d24d973ffbc797c1d5ca13014dff182113..52ea07c9e672b0b2db859b4c6f47c7a8442b5c67 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -681,6 +681,7 @@ dependencies = [
  "chrono",
  "conduit_macros",
  "const-str",
+ "ctor",
  "either",
  "figment",
  "hardened_malloc-rs",
@@ -1018,6 +1019,16 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "ctor"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
+dependencies = [
+ "quote",
+ "syn 2.0.71",
+]
+
 [[package]]
 name = "curve25519-dalek"
 version = "4.1.3"
diff --git a/Cargo.toml b/Cargo.toml
index acaed701714bd408061790c502c9a6a2ab81218c..b65ba7ad357e63b15d3c4071ec7c713e5224ca93 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,9 @@ name = "conduit"
 [workspace.dependencies.const-str]
 version = "0.5.7"
 
+[workspace.dependencies.ctor]
+version = "0.2.8"
+
 [workspace.dependencies.cargo_toml]
 version = "0.20"
 features = ["features"]
diff --git a/src/admin/mod.rs b/src/admin/mod.rs
index 7d752ff869207c68ca2a63e303739105911d95e1..cd1110ee3d0b87418ea143fa0a24cb9729a039d3 100644
--- a/src/admin/mod.rs
+++ b/src/admin/mod.rs
@@ -21,15 +21,16 @@
 extern crate conduit_core as conduit;
 extern crate conduit_service as service;
 
-pub(crate) use conduit::{mod_ctor, mod_dtor, Result};
+pub(crate) use conduit::Result;
 pub(crate) use service::services;
 
 pub(crate) use crate::utils::{escape_html, get_room_info};
 
 pub(crate) const PAGE_SIZE: usize = 100;
 
-mod_ctor! {}
-mod_dtor! {}
+conduit::mod_ctor! {}
+conduit::mod_dtor! {}
+conduit::rustc_flags_capture! {}
 
 /// Install the admin command handler
 pub async fn init() {
diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml
index 620aad02d8727fba3f315edf8c39415da0d2d057..ea772a839f35823c388757a6c20ad55d210e432a 100644
--- a/src/core/Cargo.toml
+++ b/src/core/Cargo.toml
@@ -58,6 +58,7 @@ checked_ops.workspace = true
 chrono.workspace = true
 conduit-macros.workspace = true
 const-str.workspace = true
+ctor.workspace = true
 either.workspace = true
 figment.workspace = true
 http-body-util.workspace = true
diff --git a/src/core/info/mod.rs b/src/core/info/mod.rs
index 7749bbdc8410f15ba27981707ebb92f8e5b97a6a..e11a602194ee737c64d1988ee99bb82b6439ab84 100644
--- a/src/core/info/mod.rs
+++ b/src/core/info/mod.rs
@@ -2,4 +2,7 @@
 //! etc information which can be queried by admins or used by developers.
 
 pub mod cargo;
+pub mod rustc;
 pub mod version;
+
+pub use conduit_macros::rustc_flags_capture;
diff --git a/src/core/info/rustc.rs b/src/core/info/rustc.rs
new file mode 100644
index 0000000000000000000000000000000000000000..048c0cd591129cf79ad8c719f8805e0dcd745528
--- /dev/null
+++ b/src/core/info/rustc.rs
@@ -0,0 +1,53 @@
+//! Information about the build related to rustc. This is a frontend interface
+//! informed by proc-macros at build time. Since the project is split into
+//! several crates, lower-level information is supplied from each crate during
+//! static initialization.
+
+use std::{
+	collections::BTreeMap,
+	sync::{Mutex, OnceLock},
+};
+
+use crate::utils::exchange;
+
+/// Raw capture of rustc flags used to build each crate in the project. Informed
+/// by rustc_flags_capture macro (one in each crate's mod.rs). This is
+/// done during static initialization which is why it's mutex-protected and pub.
+/// Should not be written to by anything other than our macro.
+pub static FLAGS: Mutex<BTreeMap<&str, &[&str]>> = Mutex::new(BTreeMap::new());
+
+/// Processed list of enabled features across all project crates. This is
+/// generated from the data in FLAGS.
+static FEATURES: OnceLock<Vec<&'static str>> = OnceLock::new();
+
+/// List of features enabled for the project.
+pub fn features() -> &'static Vec<&'static str> { FEATURES.get_or_init(init_features) }
+
+fn init_features() -> Vec<&'static str> {
+	let mut features = Vec::new();
+	FLAGS
+		.lock()
+		.expect("locked")
+		.iter()
+		.for_each(|(_, flags)| append_features(&mut features, flags));
+
+	features.sort_unstable();
+	features.dedup();
+	features
+}
+
+fn append_features(features: &mut Vec<&'static str>, flags: &[&'static str]) {
+	let mut next_is_cfg = false;
+	for flag in flags {
+		let is_cfg = *flag == "--cfg";
+		let is_feature = flag.starts_with("feature=");
+		if exchange(&mut next_is_cfg, is_cfg) && is_feature {
+			if let Some(feature) = flag
+				.split_once('=')
+				.map(|(_, feature)| feature.trim_matches('"'))
+			{
+				features.push(feature);
+			}
+		}
+	}
+}
diff --git a/src/core/mod.rs b/src/core/mod.rs
index 749d7b080c2e72daff4030afbcff9d90b6768cd6..b302fdcc6c9b144014c304e3b7daf6caa7a06b58 100644
--- a/src/core/mod.rs
+++ b/src/core/mod.rs
@@ -12,12 +12,17 @@
 
 pub use config::Config;
 pub use error::Error;
-pub use info::{version, version::version};
+pub use info::{rustc_flags_capture, version, version::version};
 pub use pdu::{PduBuilder, PduCount, PduEvent};
 pub use server::Server;
+pub use utils::{ctor, dtor};
+
+pub use crate as conduit_core;
 
 pub type Result<T, E = Error> = std::result::Result<T, E>;
 
+rustc_flags_capture! {}
+
 #[cfg(not(conduit_mods))]
 pub mod mods {
 	#[macro_export]
diff --git a/src/core/utils/mod.rs b/src/core/utils/mod.rs
index d7b6a72a15336f8c95d26bfc8f3e9ff18916bf27..767b65a93a45fe025164ecc337405879f80dc914 100644
--- a/src/core/utils/mod.rs
+++ b/src/core/utils/mod.rs
@@ -15,6 +15,7 @@
 
 use std::cmp::{self, Ordering};
 
+pub use ::ctor::{ctor, dtor};
 pub use bytes::{increment, u64_from_bytes, u64_from_u8, u64_from_u8x8};
 pub use debug::slice_truncated as debug_slice_truncated;
 pub use hash::calculate_hash;
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 283224f6610bb207cddd1b96c84ec8309eed76e2..6446624caa2d79d8a7843fefe5f53193b614a2d6 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -23,3 +23,4 @@
 
 conduit::mod_ctor! {}
 conduit::mod_dtor! {}
+conduit::rustc_flags_capture! {}
diff --git a/src/macros/mod.rs b/src/macros/mod.rs
index 718583a4d47ab507ab265cc8f53453c21290f1d0..94ea781e317a14524642489478232f6c7661c3f8 100644
--- a/src/macros/mod.rs
+++ b/src/macros/mod.rs
@@ -1,5 +1,6 @@
 mod admin;
 mod cargo;
+mod rustc;
 mod utils;
 
 use proc_macro::TokenStream;
@@ -11,3 +12,6 @@ pub fn admin_command_dispatch(args: TokenStream, input: TokenStream) -> TokenStr
 
 #[proc_macro_attribute]
 pub fn cargo_manifest(args: TokenStream, input: TokenStream) -> TokenStream { cargo::manifest(args, input) }
+
+#[proc_macro]
+pub fn rustc_flags_capture(args: TokenStream) -> TokenStream { rustc::flags_capture(args) }
diff --git a/src/macros/rustc.rs b/src/macros/rustc.rs
new file mode 100644
index 0000000000000000000000000000000000000000..a3bab26f32369fdf0f4e63a9632f1a670c0fd5e4
--- /dev/null
+++ b/src/macros/rustc.rs
@@ -0,0 +1,27 @@
+use proc_macro::TokenStream;
+use quote::quote;
+
+pub(super) fn flags_capture(args: TokenStream) -> TokenStream {
+	let cargo_crate_name = std::env::var("CARGO_CRATE_NAME");
+	let crate_name = match cargo_crate_name.as_ref() {
+		Err(_) => return args,
+		Ok(crate_name) => crate_name.trim_start_matches("conduit_"),
+	};
+
+	let flag = std::env::args().collect::<Vec<_>>();
+	let ret = quote! {
+		#[conduit_core::ctor]
+		fn _set_rustc_flags() {
+			let flags = &[#( #flag ),*];
+			conduit_core::info::rustc::FLAGS.lock().expect("locked").insert(#crate_name, flags);
+		}
+
+		// static strings have to be yanked on module unload
+		#[conduit_core::dtor]
+		fn _unset_rustc_flags() {
+			conduit_core::info::rustc::FLAGS.lock().expect("locked").remove(#crate_name);
+		}
+	};
+
+	ret.into()
+}
diff --git a/src/main/main.rs b/src/main/main.rs
index b13e117db97b2b4c97d8ff3e03064bc20e11a2f2..b8cb24ff1650a8d5ddbdc8b6cf2a404a29cfcf16 100644
--- a/src/main/main.rs
+++ b/src/main/main.rs
@@ -14,7 +14,7 @@
 	time::Duration,
 };
 
-use conduit::{debug_info, error, utils::available_parallelism, Error, Result};
+use conduit::{debug_info, error, rustc_flags_capture, utils::available_parallelism, Error, Result};
 use server::Server;
 use tokio::runtime;
 
@@ -22,6 +22,8 @@
 const WORKER_MIN: usize = 2;
 const WORKER_KEEPALIVE: u64 = 36;
 
+rustc_flags_capture! {}
+
 fn main() -> Result<(), Error> {
 	let args = clap::parse();
 	let runtime = runtime::Builder::new_multi_thread()
diff --git a/src/router/mod.rs b/src/router/mod.rs
index 03c70f6d72e08b11c5ef5838dddbe7f397816b83..13fe390877f5980407bfad38d4251bddaf8b9bac 100644
--- a/src/router/mod.rs
+++ b/src/router/mod.rs
@@ -14,6 +14,7 @@
 
 conduit::mod_ctor! {}
 conduit::mod_dtor! {}
+conduit::rustc_flags_capture! {}
 
 #[no_mangle]
 pub extern "Rust" fn start(server: &Arc<Server>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
diff --git a/src/service/mod.rs b/src/service/mod.rs
index ce106809cf118ef2f00726a6402c8b7abdc99489..b6ec58b5cbae5c833b231553b328693625f87456 100644
--- a/src/service/mod.rs
+++ b/src/service/mod.rs
@@ -37,6 +37,7 @@
 
 conduit::mod_ctor! {}
 conduit::mod_dtor! {}
+conduit::rustc_flags_capture! {}
 
 static SERVICES: RwLock<Option<&Services>> = RwLock::new(None);