diff --git a/crates/tor-dirmgr/Cargo.toml b/crates/tor-dirmgr/Cargo.toml
index 70323b0fb85d2754b4a3e4c5c098e406b2c8adb4..e195b0d4215d71ebd3a395f872f623369d8c6529 100644
--- a/crates/tor-dirmgr/Cargo.toml
+++ b/crates/tor-dirmgr/Cargo.toml
@@ -17,6 +17,12 @@ static = ["rusqlite/bundled"]
 # (Incomplete) support for downloading and storing router descriptors
 routerdesc = ["tor-dirclient/routerdesc"]
 
+# Enable experimental APIs that are not yet officially supported.
+#
+# These APIs are not covered by semantic versioning.  Using this
+# feature voids your "semver warrantee".
+experimental-api = []
+
 [dependencies]
 tor-basic-utils = { path="../tor-basic-utils", version = "0.1.0"}
 retry-error = { path = "../retry-error", version = "0.1.0"}
diff --git a/crates/tor-dirmgr/src/config.rs b/crates/tor-dirmgr/src/config.rs
index 536d6bf4d82a8aa02cf1dd92446f4b72374c0c67..7e889d60f9522ab229731dad90614fee9a9a4b24 100644
--- a/crates/tor-dirmgr/src/config.rs
+++ b/crates/tor-dirmgr/src/config.rs
@@ -257,28 +257,28 @@ impl DirMgrConfig {
     }
 
     /// Return the configured cache path.
-    pub(crate) fn cache_path(&self) -> &std::path::Path {
+    pub fn cache_path(&self) -> &std::path::Path {
         self.cache_path.as_ref()
     }
 
     /// Return a slice of the configured authorities
-    pub(crate) fn authorities(&self) -> &[Authority] {
+    pub fn authorities(&self) -> &[Authority] {
         self.network_config.authorities()
     }
 
     /// Return the configured set of fallback directories
-    pub(crate) fn fallbacks(&self) -> &[FallbackDir] {
+    pub fn fallbacks(&self) -> &[FallbackDir] {
         self.network_config.fallbacks()
     }
 
     /// Return set of configured networkstatus parameter overrides.
-    pub(crate) fn override_net_params(&self) -> &netstatus::NetParams<i32> {
+    pub fn override_net_params(&self) -> &netstatus::NetParams<i32> {
         &self.override_net_params
     }
 
     /// Return the schedule configuration we should use to decide when to
     /// attempt and retry downloads.
-    pub(crate) fn schedule(&self) -> &DownloadScheduleConfig {
+    pub fn schedule(&self) -> &DownloadScheduleConfig {
         &self.schedule_config
     }
 
@@ -286,7 +286,7 @@ impl DirMgrConfig {
     /// `self` are replaced with those from  `new_config`.
     ///
     /// Any fields which aren't allowed to change at runtime are copied from self.
-    pub(crate) fn update_config(&self, new_config: &DirMgrConfig) -> DirMgrConfig {
+    pub fn update_config(&self, new_config: &DirMgrConfig) -> DirMgrConfig {
         DirMgrConfig {
             cache_path: self.cache_path.clone(),
             network_config: NetworkConfig {
diff --git a/crates/tor-dirmgr/src/lib.rs b/crates/tor-dirmgr/src/lib.rs
index 926430e31c4ba82fde1b6d51a71742aa77234bcf..a0d6f5c47c250da132c48f9045f2ca4857a280c4 100644
--- a/crates/tor-dirmgr/src/lib.rs
+++ b/crates/tor-dirmgr/src/lib.rs
@@ -56,7 +56,7 @@
 
 pub mod authority;
 mod bootstrap;
-mod config;
+pub mod config;
 mod docid;
 mod docmeta;
 mod err;
@@ -67,7 +67,10 @@ mod state;
 mod storage;
 
 use crate::docid::{CacheUsage, ClientRequest, DocQuery};
+#[cfg(not(feature = "experimental-api"))]
 use crate::shared_ref::SharedMutArc;
+#[cfg(feature = "experimental-api")]
+pub use crate::shared_ref::SharedMutArc;
 use crate::storage::DynStore;
 use postage::watch;
 pub use retry::DownloadSchedule;
diff --git a/crates/tor-dirmgr/src/shared_ref.rs b/crates/tor-dirmgr/src/shared_ref.rs
index 38630b6e84c67c4a004bfb5ee5c75bf8885b993a..d1b83b9b29eaf7091738ddc6bbbf5e956dbf5a0e 100644
--- a/crates/tor-dirmgr/src/shared_ref.rs
+++ b/crates/tor-dirmgr/src/shared_ref.rs
@@ -16,7 +16,8 @@ use crate::{Error, Result};
 // sure we don't hold the lock against any async suspend points.
 #[derive(Debug, Educe)]
 #[educe(Default)]
-pub(crate) struct SharedMutArc<T> {
+#[cfg_attr(not(feature = "experimental-api"), allow(unreachable_pub))]
+pub struct SharedMutArc<T> {
     /// Locked reference to the current value.
     ///
     /// (It's okay to use RwLock here, because we never suspend
@@ -24,14 +25,15 @@ pub(crate) struct SharedMutArc<T> {
     dir: RwLock<Option<Arc<T>>>,
 }
 
+#[cfg_attr(not(feature = "experimental-api"), allow(unreachable_pub))]
 impl<T> SharedMutArc<T> {
     /// Construct a new empty SharedMutArc.
-    pub(crate) fn new() -> Self {
+    pub fn new() -> Self {
         SharedMutArc::default()
     }
 
     /// Replace the current value with `new_val`.
-    pub(crate) fn replace(&self, new_val: T) {
+    pub fn replace(&self, new_val: T) {
         let mut w = self
             .dir
             .write()
@@ -50,7 +52,7 @@ impl<T> SharedMutArc<T> {
     }
 
     /// Return a new reference to the current value, if there is one.
-    pub(crate) fn get(&self) -> Option<Arc<T>> {
+    pub fn get(&self) -> Option<Arc<T>> {
         let r = self
             .dir
             .read()
@@ -72,7 +74,7 @@ impl<T> SharedMutArc<T> {
     /// and future attempts to use it will panic. (TODO: Fix this.)
     // Note: If we decide to make this type public, we'll probably
     // want to fiddle with how we handle the return type.
-    pub(crate) fn mutate<F, U>(&self, func: F) -> Result<U>
+    pub fn mutate<F, U>(&self, func: F) -> Result<U>
     where
         F: FnOnce(&mut T) -> Result<U>,
         T: Clone,