diff --git a/Cargo.toml b/Cargo.toml
index 556703e..a012dfd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,9 +7,6 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-serde = { version = "1.0", features = ["derive"] }
-serde_json = { version="1.0", features= ["preserve_order"] }
-json_dotpath = "0.1.2"
 rand = "0.7.2"
-rocket = { version="0.4.2", default-features = false}
+rocket = "0.4.2"
 parking_lot = "0.10.0"
diff --git a/README.md b/README.md
index 5b7a343..c215d3a 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,27 @@
 # Sessions for Rocket.rs
 
-Adding cookie-based sessions to a rocket application is extremely simple:
+Adding cookie-based sessions to a rocket application is extremely simple with this crate.
+
+The implementation is generic to support any type as session data: a custom struct, `String`,
+`HashMap`, or perhaps `serde_json::Value`. You're free to choose.
+
+The session expiry time is configurable through the Fairing. When a session expires,
+the data associated with it is dropped.
+
+## Basic example
+
+This simple example uses u64 as the session variable; note that it can be a struct, map, or anything else,
+it just needs to implement `Send + Sync + Default`. 
 
 ```rust
 #![feature(proc_macro_hygiene, decl_macro)]
 #[macro_use] extern crate rocket;
 
-use rocket_session::Session;
 use std::time::Duration;
 
+// It's convenient to define a type alias:
+pub type Session<'a> = rocket_session::Session<'a, u64>;
+
 fn main() {
     rocket::ignite()
         .attach(Session::fairing(Duration::from_secs(3600)))
@@ -18,16 +31,66 @@ fn main() {
 
 #[get("/")]
 fn index(session: Session) -> String {
-    let mut count: usize = session.get_or_default("count");
-    count += 1;
-    session.set("count", count);
+    let count = session.tap(|n| {
+        // Change the stored value (it is &mut) 
+        *n += 1;
+
+        // Return something to the caller. 
+        // This can be any type, 'tap' is generic.        
+        *n
+    });
 
     format!("{} visits", count)
 }
 ```
 
-Anything serializable can be stored in the session, just make sure to unpack it to the right type.
+## Extending by a trait
+
+The `tap` method is powerful, but sometimes you may wish for something more convenient.
+
+Here is an example of using a custom trait and the `json_dotpath` crate to implement
+a polymorphic store based on serde serialization:
+
+```rust
+use serde_json::Value;
+use serde::de::DeserializeOwned;
+use serde::Serialize;
+use json_dotpath::DotPaths;
+
+pub type Session<'a> = rocket_session::Session<'a, serde_json::Map<String, Value>>;
+
+pub trait SessionAccess {
+    fn get<T: DeserializeOwned>(&self, path: &str) -> Option<T>;
 
-The session driver internally uses `serde_json::Value` and the `json_dotpath` crate. 
-Therefore, it's possible to use dotted paths and store the session data in a more structured way.
+    fn take<T: DeserializeOwned>(&self, path: &str) -> Option<T>;
+
+    fn replace<O: DeserializeOwned, N: Serialize>(&self, path: &str, new: N) -> Option<O>;
+
+    fn set<T: Serialize>(&self, path: &str, value: T);
+
+    fn remove(&self, path: &str) -> bool;
+}
+
+impl<'a> SessionAccess for Session<'a> {
+    fn get<T: DeserializeOwned>(&self, path: &str) -> Option<T> {
+        self.tap(|data| data.dot_get(path))
+    }
+
+    fn take<T: DeserializeOwned>(&self, path: &str) -> Option<T> {
+        self.tap(|data| data.dot_take(path))
+    }
+
+    fn replace<O: DeserializeOwned, N: Serialize>(&self, path: &str, new: N) -> Option<O> {
+        self.tap(|data| data.dot_replace(path, new))
+    }
+
+    fn set<T: Serialize>(&self, path: &str, value: T) {
+        self.tap(|data| data.dot_set(path, value));
+    }
+
+    fn remove(&self, path: &str) -> bool {
+        self.tap(|data| data.dot_remove(path))
+    }
+}
+```
 
diff --git a/src/session.rs b/src/session.rs
index 86892a7..583a011 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -1,20 +1,20 @@
 use parking_lot::RwLock;
 use rand::Rng;
-use rocket::fairing::{self, Fairing, Info};
-use rocket::request::FromRequest;
 
 use rocket::{
+    fairing::{self, Fairing, Info},
     http::{Cookie, Status},
+    request::FromRequest,
     Outcome, Request, Response, Rocket, State,
 };
 
-use serde::export::PhantomData;
 use std::collections::HashMap;
+use std::marker::PhantomData;
 use std::ops::Add;
 use std::time::{Duration, Instant};
 
 const SESSION_COOKIE: &str = "SESSID";
-const SESSION_ID_LEN : usize = 16;
+const SESSION_ID_LEN: usize = 16;
 
 /// Session, as stored in the sessions store
 #[derive(Debug)]
@@ -30,7 +30,7 @@ where
 
 /// Session store (shared state)
 #[derive(Default, Debug)]
-struct SessionStore<D>
+pub struct SessionStore<D>
 where
     D: 'static + Sync + Send + Default,
 {
@@ -40,6 +40,17 @@ where
     lifespan: Duration,
 }
 
+impl<D> SessionStore<D>
+where
+    D: 'static + Sync + Send + Default,
+{
+    /// Remove all expired sessions
+    pub fn remove_expired(&self) {
+        let now = Instant::now();
+        self.inner.write().retain(|_k, v| v.expires > now);
+    }
+}
+
 /// Session ID newtype for rocket's "local_cache"
 #[derive(PartialEq, Hash, Clone, Debug)]
 struct SessionID(String);
@@ -112,16 +123,40 @@ where
         }
     }
 
+    /// Access the session store
+    pub fn get_store(&self) -> &SessionStore<D> {
+        &self.store
+    }
+
+    /// Set the session object to its default state
+    pub fn reset(&self) {
+        self.tap(|m| {
+            *m = D::default();
+        })
+    }
+
+    /// Renew the session without changing any data
+    pub fn renew(&self) {
+        self.tap(|_| ())
+    }
+
     /// Run a closure with a mutable reference to the session object.
     /// The closure's return value is send to the caller.
     pub fn tap<T>(&self, func: impl FnOnce(&mut D) -> T) -> T {
         let mut wg = self.store.inner.write();
         if let Some(instance) = wg.get_mut(&self.id.0) {
+            // wipe session data if expired
+            if instance.expires <= Instant::now() {
+                instance.data = D::default();
+            }
+            // update expiry timestamp
             instance.expires = Instant::now().add(self.store.lifespan);
+
             func(&mut instance.data)
         } else {
+            // no object in the store yet, start fresh
             let mut data = D::default();
-            let rv = func(&mut data);
+            let result = func(&mut data);
             wg.insert(
                 self.id.0.clone(),
                 SessionInstance {
@@ -129,14 +164,9 @@ where
                     expires: Instant::now().add(self.store.lifespan),
                 },
             );
-            rv
+            result
         }
     }
-
-    /// Renew the session
-    pub fn renew(&self) {
-        self.tap(|_| ())
-    }
 }
 
 /// Fairing struct