diff --git a/src/lib.rs b/src/lib.rs
index ed15ad6..faf1731 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,11 +3,13 @@ pub mod mutators;
 pub mod nav;
 pub mod search;
 
+use std::cell::UnsafeCell;
 use std::hash::Hash;
+use std::ops::Deref;
 
 use base::{EdgeId, RawEdge, RawVertex, VertexId};
 use mutators::{MutEdge, MutNode};
-use nav::Node;
+use nav::{Edge, Node};
 use symbol_map::indexing::{Indexing, Insertion};
 use symbol_map::SymbolId;
 
@@ -26,7 +28,7 @@ use symbol_map::SymbolId;
 ///
 /// Vertices are addressed by content. To examine graph contents, obtain a node
 /// handle with `get_node`. To modify graph contents, add new root vertices with
-/// `add_root` and retrieve extant vertices with `get_node_mut`.
+/// `add_node` and retrieve extant vertices with `get_node_mut`.
 pub struct Graph<T, S, A>
 where
   T: Hash + Eq + Clone,
@@ -123,14 +125,14 @@ where
     }
   }
 
-  /// Adds a root vertex (one with no parents) for the given game state and
+  /// Adds a vertex (with no parents or children) for the given game state and
   /// data and returns a mutable handle for it.
   ///
   /// If `state` is already known, returns a mutable handle to that state,
   /// ignoring the `data` parameter. As a result, this method is guaranteed to
   /// return a handle for a root vertex only when `state` is a novel game
   /// state.
-  pub fn add_root<'s>(&'s mut self, state: T, data: S) -> MutNode<'s, T, S, A> {
+  pub fn add_node<'s>(&'s mut self, state: T, data: S) -> MutNode<'s, T, S, A> {
     let node_id = match self.state_ids.get_or_insert(state).map(|s| s.id().clone()) {
       Insertion::Present(id) => id,
       Insertion::New(id) => {
@@ -214,6 +216,94 @@ where
   }
 }
 
+/// Allows both read-only references into a `Graph` and operations that modify
+/// the graph but do not invalidate family of types defined in the `nav`
+/// module.
+///
+/// All methods on `Graph` that take a `&self` parameter may also be called on
+/// an `AppendOnlyGraph`. The `add_node` and `add_edge` methods may also be
+/// called, because they do not invalidate `Node`, `Edge`, or other such smart
+/// pointers into the underlying graph. Unlike the analogous methods on `Graph`,
+/// these methods return a read-only view of the graph topology, instead of
+/// granting read-write access.
+///
+/// For example:
+///
+/// ```rust
+/// # use search_graph::{AppendOnlyGraph, Graph};
+/// # use search_graph::nav::{Node, Edge};
+/// # fn main() {
+/// let appendable: AppendOnlyGraph<u32, String, ()> = Graph::new().into();
+/// let root1 = appendable.add_node(0, "data1".to_string());
+/// // If appendable were a Graph, we could not call add_node while root1 is alive.
+/// let root2 = appendable.add_node(1, "data2".to_string());
+/// assert_eq!("data1", appendable.get_node(&0).unwrap().get_data());
+/// # }
+/// ```
+pub struct AppendOnlyGraph<T, S, A>
+where
+  T: Hash + Eq + Clone,
+{
+  graph: UnsafeCell<Graph<T, S, A>>,
+}
+
+impl<T, S, A> AppendOnlyGraph<T, S, A>
+where
+  T: Hash + Eq + Clone,
+{
+  pub fn add_node<'s>(&'s self, state: T, data: S) -> Node<'s, T, S, A> {
+    unsafe { (*self.graph.get()).add_node(state, data).to_node() }
+  }
+
+  pub fn add_edge<'s, F, G>(
+    &'s mut self,
+    source: T,
+    source_data: F,
+    dest: T,
+    dest_data: G,
+    edge_data: A,
+  ) -> Edge<'s, T, S, A>
+  where
+    F: for<'b> FnOnce(Node<'b, T, S, A>) -> S,
+    G: for<'b> FnOnce(Node<'b, T, S, A>) -> S,
+  {
+    unsafe {
+      (*self.graph.get())
+        .add_edge(source, source_data, dest, dest_data, edge_data)
+        .to_edge()
+    }
+  }
+}
+
+impl<T, S, A> Deref for AppendOnlyGraph<T, S, A>
+where
+  T: Hash + Eq + Clone,
+{
+  type Target = Graph<T, S, A>;
+
+  fn deref(&self) -> &Graph<T, S, A> {
+    unsafe { &*self.graph.get() }
+  }
+}
+
+impl<T, S, A> From<AppendOnlyGraph<T, S, A>> for Graph<T, S, A>
+where
+  T: Hash + Eq + Clone,
+{
+  fn from(graph: AppendOnlyGraph<T, S, A>) -> Self {
+    graph.graph.into_inner()
+  }
+}
+
+impl<T, S, A> From<Graph<T, S, A>> for AppendOnlyGraph<T, S, A>
+where
+  T: Hash + Eq + Clone,
+{
+  fn from(graph: Graph<T, S, A>) -> Self {
+    AppendOnlyGraph { graph: UnsafeCell::new(graph) }
+  }
+}
+
 #[cfg(test)]
 mod test {
   use crossbeam_utils::thread;
diff --git a/src/mutators/mark_compact.rs b/src/mutators/mark_compact.rs
index 506b1b4..aff7e2b 100644
--- a/src/mutators/mark_compact.rs
+++ b/src/mutators/mark_compact.rs
@@ -290,9 +290,9 @@ mod test {
   #[test]
   fn mark_roots_ok() {
     let mut g = empty_graph();
-    g.add_root("0", "");
-    g.add_root("1", "");
-    g.add_root("2", "");
+    g.add_node("0", "");
+    g.add_node("1", "");
+    g.add_node("2", "");
     assert_eq!(3, g.vertex_count());
     assert_eq!(0, g.edge_count());
     let root_ids = [VertexId(0), VertexId(1), VertexId(2)];
diff --git a/src/search.rs b/src/search.rs
index 8773055..809765c 100644
--- a/src/search.rs
+++ b/src/search.rs
@@ -345,7 +345,7 @@ mod test {
   #[test]
   fn instantiation_ok() {
     let mut g = Graph::new();
-    let root = g.add_root("root", "root");
+    let root = g.add_node("root", "root");
 
     let path = Stack::new(root);
     assert_eq!(1, path.len());
@@ -355,7 +355,7 @@ mod test {
   #[test]
   fn push_no_children_ok() {
     let mut g = Graph::new();
-    let root = g.add_root("root", "root");
+    let root = g.add_node("root", "root");
 
     let mut path = Stack::new(root);
     assert_eq!(1, path.len());
@@ -377,7 +377,7 @@ mod test {
   #[test]
   fn push_no_children_err() {
     let mut g = Graph::new();
-    let root = g.add_root("root", "root");
+    let root = g.add_node("root", "root");
 
     let mut path = Stack::new(root);
     assert_eq!(1, path.len());
@@ -478,7 +478,7 @@ mod test {
   #[test]
   fn push_no_parents_ok() {
     let mut g = Graph::new();
-    let root = g.add_root("root", "root");
+    let root = g.add_node("root", "root");
 
     let mut path = Stack::new(root);
     assert_eq!(1, path.len());
@@ -500,7 +500,7 @@ mod test {
   #[test]
   fn push_no_parents_err() {
     let mut g = Graph::new();
-    let root = g.add_root("root", "root");
+    let root = g.add_node("root", "root");
 
     let mut path = Stack::new(root);
     assert_eq!(1, path.len());
@@ -633,9 +633,9 @@ mod test {
   #[test]
   fn search_path_iter_empty_ok() {
     let mut g = Graph::new();
-    g.add_root("root", "root");
+    g.add_node("root", "root");
 
-    let path = Stack::new(g.add_root("root", "root"));
+    let path = Stack::new(g.add_node("root", "root"));
     assert_eq!(1, path.len());
     assert_eq!("root", *path.head().get_data());
 
@@ -651,7 +651,7 @@ mod test {
   #[test]
   fn search_path_iter_items_ok() {
     let mut g = Graph::new();
-    g.add_root("root", "root");
+    g.add_node("root", "root");
     add_edge(&mut g, "root", "A");
     add_edge(&mut g, "A", "B");
 
@@ -706,7 +706,7 @@ mod test {
   fn pop_empty_is_none_ok() {
     let mut g = Graph::new();
 
-    let mut path = Stack::new(g.add_root("root", "root"));
+    let mut path = Stack::new(g.add_node("root", "root"));
     assert_eq!(1, path.len());
     assert!(path.pop().is_none());
   }