Repository: sopherapps/pydantic-redis (language: python)

Issue:
Add a Documentations Site
Create a documentations site say on [github pages](https://pages.github.com/) or [read the docs](https://readthedocs.org/projects/readmydoc/)

TODO:
- [ ] Set up the devops parts of document generation via github actions
- [ ] Enhance the doc strings within the code
- [ ] Add the docs site link to the github repo home page


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fc0875e4..fc1c8606 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,6 +61,9 @@ jobs:
       - name: Build
         run: |
           python -m build
+      - name: Deploy docs
+        run: |
+          mkdocs gh-deploy -c
       - name: Upload to pypi
         run: |
           twine upload -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} dist/*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adf2d800..03242bb0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [Unreleased]
 
+## [0.4.3] - 2022-12-29
+
+### Added
+
+- Added mkdocs documentation
+
+### Changed
+
+### Fixed
+
+- Fixed docs building in CI
+
 ## [0.4.2] - 2022-12-29
 
 ### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c5126ea4..44892029 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,6 +60,50 @@ People *love* thorough bug reports. I'm not even kidding.
 
 By contributing, you agree that your contributions will be licensed under its MIT License.
 
+## How to test
+
+- Clone the repo and enter its root folder
+
+  ```bash
+  git clone https://github.com/sopherapps/pydantic-redis.git && cd pydantic-redis
+  ```
+
+- Create a virtual environment and activate it
+
+  ```bash
+  virtualenv -p /usr/bin/python3.6 env && source env/bin/activate
+  ```
+
+- Install the dependencies
+
+  ```bash
+  pip install -r requirements.txt
+  ```
+
+- Run the pre-commit installation
+
+  ```bash
+  pre-commit install
+  ```
+
+- Run the tests command
+
+  ```bash
+  pytest --benchmark-disable
+  ```
+
+- Run benchmarks
+
+  ```bash
+  pytest --benchmark-compare --benchmark-autosave
+  ```
+
+- Or run to get benchmarks summary
+
+  ```shell
+  pytest test/test_benchmarks.py --benchmark-columns=mean,min,max --benchmark-name=short
+  ```
+
 ## References
 
 This document was adapted from [a gist by Brian A. Danielak](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62) which
diff --git a/README.md b/README.md
index ab2ecbf5..a58e0b06 100644
--- a/README.md
+++ b/README.md
@@ -4,13 +4,15 @@
 
 A simple declarative ORM for Redis
 
-## Main Dependencies
+---
 
-- [Python +3.6](https://www.python.org)
-- [redis](https://pypi.org/project/redis/)
-- [pydantic](https://github.com/samuelcolvin/pydantic/)
+**Documentation:** [https://sopherapps.github.io/pydantic-redis](https://sopherapps.github.io/pydantic-redis)
 
-## Most Notable Features
+**Source Code:** [https://github.com/sopherapps/pydantic-redis](https://github.com/sopherapps/pydantic-redis)
+
+--- 
+
+Most Notable Features are:
 
 - Define business domain objects as [pydantic](https://github.com/samuelcolvin/pydantic/) and automatically get ability
   to save them as is in [redis](https://pypi.org/project/redis/) with an intuitive API of `insert`, `update`, `delete`,
@@ -20,462 +22,6 @@ A simple declarative ORM for Redis
   them when queried again from redis.
 - Both synchronous and asynchronous APIs available.
 
-## Getting Started (Synchronous Version)
-
-- Install the package
-
-  ```bash
-  pip install pydantic-redis
-  ```
-
-- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis` and use accordingly
-
-```python
-from datetime import date
-from typing import Tuple, List, Optional
-from pydantic_redis import RedisConfig, Model, Store
-
-
-class Author(Model):
-  """
-  An Author model, just like a pydantic model with appropriate 
-  type annotations
-  NOTE: The `_primary_key_field` is mandatory
-  """
-  _primary_key_field: str = 'name'
-  name: str
-  active_years: Tuple[int, int]
-
-
-class Book(Model):
-  """
-  A Book model.
-  
-  Models can have the following field types
-  - The usual i.e. float, int, dict, list, date, str, dict, Optional etc
-    as long as they are serializable by orjson
-  - Nested models e.g. `author: Author` or `author: Optional[Author]`
-  - List of nested models e.g. `authors: List[Author]` 
-    or `authors: Optional[List[Author]]`
-  - Tuples including nested models e.g. `access_log: Tuple[Author, date]` 
-    or `access_log: Optional[Tuple[Author, date]]]`
-  
-  NOTE: 1. Any nested model whether plain or in a list or tuple will automatically 
-        inserted into the redis store when the parent model is inserted. 
-        e.g. a Book with an author field, when inserted, will also insert
-       the author. The author can then be queried directly if that's something 
-       one wishes to do.
-       
-       2. When a parent model is inserted with a nested model instance that 
-       already exists, the older nested model instance is overwritten. 
-       This is one way of updating nested models. 
-       All parent models that contain that nested model instance will see the change. 
-  """
-  _primary_key_field: str = 'title'
-  title: str
-  author: Author
-  rating: float
-  published_on: date
-  tags: List[str] = []
-  in_stock: bool = True
-
-
-class Library(Model):
-  """
-  A library model.
-  
-  It shows a number of complicated nested models.
-  
-  About Nested Model Performance
-  ---
-  To minimize the performance penalty for nesting models, 
-  we use REDIS EVALSHA to eagerly load the nested models
-  before the response is returned to the client.
-  This ensures that only ONE network call is made every time.
-  """
-  _primary_key_field: str = 'name'
-  name: str
-  address: str
-  books: List[Book] = None
-  lost: Optional[List[Book]] = None
-  popular: Optional[Tuple[Book, Book]] = None
-  new: Tuple[Book, Author, int] = None
-
-
-# Create the store
-store = Store(
-  name='some_name', 
-  redis_config=RedisConfig(db=5, host='localhost', port=6379),
-  life_span_in_seconds=3600)
-
-# register your models. DON'T FORGET TO DO THIS.
-store.register_model(Book)
-store.register_model(Library)
-store.register_model(Author)
-
-# sample authors. You can create as many as you wish anywhere in the code
-authors = {
-  "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
-  "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
-}
-
-# Sample books.
-books = [
-    Book(
-        title="Oliver Twist",
-        author=authors["charles"],
-        published_on=date(year=1215, month=4, day=4),
-        in_stock=False,
-        rating=2,
-        tags=["Classic"],
-    ),
-    Book(
-        title="Great Expectations",
-        author=authors["charles"],
-        published_on=date(year=1220, month=4, day=4),
-        rating=5,
-        tags=["Classic"],
-    ),
-    Book(
-        title="Jane Eyre",
-        author=authors["charles"],
-        published_on=date(year=1225, month=6, day=4),
-        in_stock=False,
-        rating=3.4,
-        tags=["Classic", "Romance"],
-    ),
-    Book(
-        title="Wuthering Heights",
-        author=authors["jane"],
-        published_on=date(year=1600, month=4, day=4),
-        rating=4.0,
-        tags=["Classic", "Romance"],
-    ),
-]
-
-# Some library objects
-libraries = [
-  Library(
-    name="The Grand Library", 
-    address="Kinogozi, Hoima, Uganda", 
-    lost=[books[1]],
-  ),
-  Library(
-    name="Christian Library", 
-    address="Buhimba, Hoima, Uganda", 
-    new=(books[0], authors["jane"], 30),
-  )
-]
-
-# Insert Many. You can given them a TTL (life_span_seconds).
-Book.insert(books, life_span_seconds=3600)
-Library.insert(libraries)
-
-# Insert One. You can also given it a TTL (life_span_seconds).
-Author.insert(Author(name="Jack Myers", active_years=(1240, 1300)))
-
-# Update One. You can also given it a TTL (life_span_seconds).
-Book.update(_id="Oliver Twist", data={"author": authors["jane"]})
-
-# Update nested model indirectly
-updated_jane = Author(**authors["jane"].dict())
-updated_jane.active_years = (1999, 2008)
-Book.update(_id="Oliver Twist", data={"author": updated_jane})
-
-# Query the data
-# Get all, with all fields shown. Data returned is a list of models instances.
-all_books = Book.select()
-print(all_books)
-# Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), 
-# in_stock=False), Book(...]
-
-# or paginate i.e. skip some books and return only upto a given number
-paginated_books = Book.select(skip=2, limit=2)
-print(paginated_books)
-
-# Get some, with all fields shown. Data returned is a list of models instances.
-some_books = Book.select(ids=["Oliver Twist", "Jane Eyre"])
-print(some_books)
-
-# Note: Pagination does not work when ids are provided i.e.
-assert some_books == Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10)
-
-# Get all, with only a few fields shown. Data returned is a list of dictionaries.
-books_with_few_fields = Book.select(columns=["author", "in_stock"])
-print(books_with_few_fields)
-# Prints [{"author": "'Charles Dickens", "in_stock": "True"},...]
-
-# or paginate i.e. skip some books and return only upto a given number
-paginated_books_with_few_fields = Book.select(columns=["author", "in_stock"], skip=2, limit=2)
-print(paginated_books_with_few_fields)
-
-# Get some, with only some fields shown. Data returned is a list of dictionaries.
-some_books_with_few_fields = Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"])
-print(some_books_with_few_fields)
-
-# Query the nested models directly.
-some_authors = Author.select(ids=["Jane Austen"])
-print(some_authors)
-
-# Delete any number of items
-Library.delete(ids=["The Grand Library"])
-```
-
-## Getting Started (Asynchronous Version)
-
-- Install the package
-
-  ```bash
-  pip install pydantic-redis
-  ```
-
-- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis.asyncio` and use accordingly
-
-```python
-import asyncio
-from datetime import date
-from typing import Tuple, List, Optional
-from pydantic_redis.asyncio import RedisConfig, Model, Store
-
-# The features are exactly the same as the synchronous version, 
-# except for the ability to return coroutines when `insert`, 
-# `update`, `select` or `delete` are called.
-
-
-class Author(Model):
-  """
-  An Author model, just like a pydantic model with appropriate 
-  type annotations
-  NOTE: The `_primary_key_field` is mandatory
-  """
-  _primary_key_field: str = 'name'
-  name: str
-  active_years: Tuple[int, int]
-
-
-class Book(Model):
-  """
-  A Book model.
-  
-  Models can have the following field types
-  - The usual i.e. float, int, dict, list, date, str, dict, Optional etc
-    as long as they are serializable by orjson
-  - Nested models e.g. `author: Author` or `author: Optional[Author]`
-  - List of nested models e.g. `authors: List[Author]` 
-    or `authors: Optional[List[Author]]`
-  - Tuples including nested models e.g. `access_log: Tuple[Author, date]` 
-    or `access_log: Optional[Tuple[Author, date]]]`
-  
-  NOTE: 1. Any nested model whether plain or in a list or tuple will automatically 
-        inserted into the redis store when the parent model is inserted. 
-        e.g. a Book with an author field, when inserted, will also insert
-       the author. The author can then be queried directly if that's something 
-       one wishes to do.
-       
-       2. When a parent model is inserted with a nested model instance that 
-       already exists, the older nested model instance is overwritten. 
-       This is one way of updating nested models. 
-       All parent models that contain that nested model instance will see the change. 
-  """
-  _primary_key_field: str = 'title'
-  title: str
-  author: Author
-  rating: float
-  published_on: date
-  tags: List[str] = []
-  in_stock: bool = True
-
-
-class Library(Model):
-  """
-  A library model.
-  
-  It shows a number of complicated nested models.
-  
-  About Nested Model Performance
-  ---
-  To minimize the performance penalty for nesting models, 
-  we use REDIS EVALSHA to eagerly load the nested models
-  before the response is returned to the client.
-  This ensures that only ONE network call is made every time.
-  """
-  _primary_key_field: str = 'name'
-  name: str
-  address: str
-  books: List[Book] = None
-  lost: Optional[List[Book]] = None
-  popular: Optional[Tuple[Book, Book]] = None
-  new: Tuple[Book, Author, int] = None
-
-
-async def run_async():
-  """The async coroutine"""
-  # Create the store
-  store = Store(
-    name='some_name', 
-    redis_config=RedisConfig(db=5, host='localhost', port=6379),
-    life_span_in_seconds=3600)
-  
-  # register your models. DON'T FORGET TO DO THIS.
-  store.register_model(Book)
-  store.register_model(Library)
-  store.register_model(Author)
-  
-  # sample authors. You can create as many as you wish anywhere in the code
-  authors = {
-    "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
-    "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
-  }
-  
-  # Sample books.
-  books = [
-      Book(
-          title="Oliver Twist",
-          author=authors["charles"],
-          published_on=date(year=1215, month=4, day=4),
-          in_stock=False,
-          rating=2,
-          tags=["Classic"],
-      ),
-      Book(
-          title="Great Expectations",
-          author=authors["charles"],
-          published_on=date(year=1220, month=4, day=4),
-          rating=5,
-          tags=["Classic"],
-      ),
-      Book(
-          title="Jane Eyre",
-          author=authors["charles"],
-          published_on=date(year=1225, month=6, day=4),
-          in_stock=False,
-          rating=3.4,
-          tags=["Classic", "Romance"],
-      ),
-      Book(
-          title="Wuthering Heights",
-          author=authors["jane"],
-          published_on=date(year=1600, month=4, day=4),
-          rating=4.0,
-          tags=["Classic", "Romance"],
-      ),
-  ]
-  
-  # Some library objects
-  libraries = [
-    Library(
-      name="The Grand Library", 
-      address="Kinogozi, Hoima, Uganda", 
-      lost=[books[1]],
-    ),
-    Library(
-      name="Christian Library", 
-      address="Buhimba, Hoima, Uganda", 
-      new=(books[0], authors["jane"], 30),
-    )
-  ]
-  
-  # Insert Many. You can given them a TTL (life_span_seconds).
-  await Book.insert(books, life_span_seconds=3600)
-  await Library.insert(libraries)
-  
-  # Insert One. You can also given it a TTL (life_span_seconds).
-  await Author.insert(Author(name="Jack Myers", active_years=(1240, 1300)))
-  
-  # Update One. You can also given it a TTL (life_span_seconds).
-  await Book.update(_id="Oliver Twist", data={"author": authors["jane"]})
-  
-  # Update nested model indirectly
-  updated_jane = Author(**authors["jane"].dict())
-  updated_jane.active_years = (1999, 2008)
-  await Book.update(_id="Oliver Twist", data={"author": updated_jane})
-  
-  # Query the data
-  # Get all, with all fields shown. Data returned is a list of models instances.
-  all_books = await Book.select()
-  print(all_books)
-  # Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), 
-  # in_stock=False), Book(...]
-  
-  # or paginate i.e. skip some books and return only upto a given number
-  paginated_books = await Book.select(skip=2, limit=2)
-  print(paginated_books)
-    
-  # Get some, with all fields shown. Data returned is a list of models instances.
-  some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
-  print(some_books)
-  
-  # Note: Pagination does not work when ids are provided i.e.
-  assert some_books == await Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10)
-  
-  # Get all, with only a few fields shown. Data returned is a list of dictionaries.
-  books_with_few_fields = await Book.select(columns=["author", "in_stock"])
-  print(books_with_few_fields)
-  # Prints [{"author": "'Charles Dickens", "in_stock": "True"},...]
-  
-  # or paginate i.e. skip some books and return only upto a given number
-  paginated_books_with_few_fields = await Book.select(columns=["author", "in_stock"], skip=2, limit=2)
-  print(paginated_books_with_few_fields)
-  
-  # Get some, with only some fields shown. Data returned is a list of dictionaries.
-  some_books_with_few_fields = await Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"])
-  print(some_books_with_few_fields)
-  
-  # Query the nested models directly.
-  some_authors = await Author.select(ids=["Jane Austen"])
-  print(some_authors)
-  
-  # Delete any number of items
-  await Library.delete(ids=["The Grand Library"])
-
-
-asyncio.run(run_async())
-```
-
-## How to test
-
-- Clone the repo and enter its root folder
-
-  ```bash
-  git clone https://github.com/sopherapps/pydantic-redis.git && cd pydantic-redis
-  ```
-
-- Create a virtual environment and activate it
-
-  ```bash
-  virtualenv -p /usr/bin/python3.6 env && source env/bin/activate
-  ```
-
-- Install the dependencies
-
-  ```bash
-  pip install -r requirements.txt
-  ```
-
-- Run the pre-commit installation
-
-  ```bash
-  pre-commit install
-  ```
-
-- Run the tests command
-
-  ```bash
-  pytest --benchmark-disable
-  ```
-
-- Run benchmarks
-
-  ```bash
-  pytest --benchmark-compare --benchmark-autosave
-  ```
-
-- Or run to get benchmarks summary
-
-  ```shell
-  pytest test/test_benchmarks.py --benchmark-columns=mean,min,max --benchmark-name=short
-  ```
-
 ## Benchmarks
 
 On an average PC ~16GB RAM, i7 Core
diff --git a/docs/change-log.md b/docs/change-log.md
new file mode 100644
index 00000000..f062fd7c
--- /dev/null
+++ b/docs/change-log.md
@@ -0,0 +1,106 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](http://keepachangelog.com/)
+and this project adheres to [Semantic Versioning](http://semver.org/).
+
+## [Unreleased]
+
+## [0.4.3] - 2022-12-29
+
+### Added
+
+- Added mkdocs documentation
+
+### Changed
+
+### Fixed
+
+- Fixed docs building in CI
+
+## [0.4.2] - 2022-12-29
+
+### Added
+
+### Changed
+
+### Fixed
+
+- Fixed unexpected data error when selecting some columns for some records
+
+## [0.4.1] - 2022-12-29
+
+### Added
+
+### Changed
+
+### Fixed
+
+## [0.4.0] - 2022-12-17
+
+### Added
+
+- Added pagination
+
+### Changed
+
+- Changed redis index to use sorted sets instead of ordinary sets
+
+### Fixed
+
+## [0.3.0] - 2022-12-15
+
+### Added
+
+- Added asyncio support, to be got from the `pydantic_redis.asyncio` module
+
+### Changed
+
+- Moved the synchronous version to the `pydantic_redis.syncio` module, but kept its contents exposed in pydantic_redis
+  for backward-compatibility
+
+### Fixed
+
+## [0.2.0] - 2022-12-15
+
+### Added
+
+### Changed
+
+- Changed the `NESTED_MODEL_LIST_FIELD_PREFIX` to "___" and `NESTED_MODEL_TUPLE_FIELD_PREFIX` to "____"
+- Changed all queries (selects) to use lua scripts
+- Changed `Model.deserialize_partially` to receive data either as a dict or as a flattened list of key-values
+
+### Fixed
+
+## [0.1.8] - 2022-12-13
+
+### Added
+
+- Add support for model fields that are tuples of nested models 
+
+### Changed
+
+### Fixed
+
+## [0.1.7] - 2022-12-12
+
+### Added
+
+### Changed
+
+### Fixed
+
+- Fixed support for model properties that are *Optional* lists of nested models 
+- Fixed issue with field names being disfigured by `lstrip` when attempting to strip nested-mode-prefixes 
+
+## [0.1.6] - 2022-11-01
+
+### Added
+
+- Support for model properties that are lists of nested models 
+
+### Changed
+
+### Fixed
\ No newline at end of file
diff --git a/docs/css/custom.css b/docs/css/custom.css
new file mode 100644
index 00000000..07470ee9
--- /dev/null
+++ b/docs/css/custom.css
@@ -0,0 +1,40 @@
+.termynal-comment {
+  color: #4a968f;
+  font-style: italic;
+  display: block;
+}
+
+.termy {
+  direction: ltr;
+}
+
+.termy [data-termynal] {
+  white-space: pre-wrap;
+}
+
+/* Indentation. */
+div.doc-contents:not(.first) {
+  padding-left: 25px;
+  border-left: .05rem solid var(--md-typeset-table-color);
+}
+
+/* Mark external links as such. */
+a.autorefs-external::after {
+  /* https://primer.style/octicons/arrow-up-right-24 */
+  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="rgb(0, 0, 0)" d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>');
+  content: ' ';
+
+  display: inline-block;
+  position: relative;
+  top: 0.1em;
+  margin-left: 0.2em;
+  margin-right: 0.1em;
+
+  height: 1em;
+  width: 1em;
+  border-radius: 100%;
+  background-color: var(--md-typeset-a-color);
+}
+a.autorefs-external:hover::after {
+  background-color: var(--md-accent-fg-color);
+}
diff --git a/docs/css/termynal.css b/docs/css/termynal.css
new file mode 100644
index 00000000..406c0089
--- /dev/null
+++ b/docs/css/termynal.css
@@ -0,0 +1,109 @@
+/**
+ * termynal.js
+ *
+ * @author Ines Montani <ines@ines.io>
+ * @version 0.0.1
+ * @license MIT
+ */
+
+:root {
+    --color-bg: #252a33;
+    --color-text: #eee;
+    --color-text-subtle: #a2a2a2;
+}
+
+[data-termynal] {
+    width: 750px;
+    max-width: 100%;
+    background: var(--color-bg);
+    color: var(--color-text);
+    /* font-size: 18px; */
+    font-size: 15px;
+    /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
+    font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
+    border-radius: 4px;
+    padding: 75px 45px 35px;
+    position: relative;
+    -webkit-box-sizing: border-box;
+            box-sizing: border-box;
+}
+
+[data-termynal]:before {
+    content: '';
+    position: absolute;
+    top: 15px;
+    left: 15px;
+    display: inline-block;
+    width: 15px;
+    height: 15px;
+    border-radius: 50%;
+    /* A little hack to display the window buttons in one pseudo element. */
+    background: #d9515d;
+    -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
+            box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
+}
+
+[data-termynal]:after {
+    content: 'bash';
+    position: absolute;
+    color: var(--color-text-subtle);
+    top: 5px;
+    left: 0;
+    width: 100%;
+    text-align: center;
+}
+
+a[data-terminal-control] {
+    text-align: right;
+    display: block;
+    color: #aebbff;
+}
+
+[data-ty] {
+    display: block;
+    line-height: 2;
+}
+
+[data-ty]:before {
+    /* Set up defaults and ensure empty lines are displayed. */
+    content: '';
+    display: inline-block;
+    vertical-align: middle;
+}
+
+[data-ty="input"]:before,
+[data-ty-prompt]:before {
+    margin-right: 0.75em;
+    color: var(--color-text-subtle);
+}
+
+[data-ty="input"]:before {
+    content: '$';
+}
+
+[data-ty][data-ty-prompt]:before {
+    content: attr(data-ty-prompt);
+}
+
+[data-ty-cursor]:after {
+    content: attr(data-ty-cursor);
+    font-family: monospace;
+    margin-left: 0.5em;
+    -webkit-animation: blink 1s infinite;
+            animation: blink 1s infinite;
+}
+
+
+/* Cursor animation */
+
+@-webkit-keyframes blink {
+    50% {
+        opacity: 0;
+    }
+}
+
+@keyframes blink {
+    50% {
+        opacity: 0;
+    }
+}
diff --git a/docs/explanation/why-use-orms.md b/docs/explanation/why-use-orms.md
new file mode 100644
index 00000000..0d4fe6be
--- /dev/null
+++ b/docs/explanation/why-use-orms.md
@@ -0,0 +1,83 @@
+# Why Use ORMs
+
+An object-relational-mapping (ORM) makes writing business logic intuitive 
+because the data representation is closer to what the real-life situation is. 
+It helps decouple the way such data is programmed from the way such data is 
+actually persisted in any of the data persistence technologies we have, 
+typically a database.
+
+Take the example of a book.
+In code, one will represent a book as an object with a number of properties such as "title", "edition", "author" etc.
+
+```python
+class Book(Model):
+  title: str
+  edition: int
+  author: Author
+```
+
+However, in the underlying data store, the same book could be saved as say,
+a row in a table for a relational database like PostgreSQL,
+or as a document in a document-based NoSQL databases like MongoDB 
+or as a hashmap in redis.
+Of these, the document-based NoSQL databases are the closest to the definition in code.
+
+For MongoDB, the same book might be represented as the object below:
+
+```json
+{
+  "id": "some-random-string",
+  "title": "the title of the book",
+  "edition": 2,
+  "author": {
+    "name": "Charles Payne",
+    "yearsActive": [
+      1992,
+      2008
+    ]
+  }
+}
+```
+
+As you can see, it is still quite different.
+
+However, for redis, the representation is even going to be further off.
+It will most likely be saved as hashmap, with a given key. 
+The properties of book will be 'fields' for that hashmap.
+
+In order to interact with the book representation in the redis server, 
+one has to write commands like:
+
+```shell
+# to save the book in the data store
+HSET "some key" "title" "the title of the book" "edition" 2 "author" "{\"name\":\"Charles Payne\",\"yearsActive\":[1992,2008]}"
+# to retrieve the entire book
+HGETALL "some key"
+# to retrieve just a few details of the book
+HMGET "some key" "title" "edition"
+# to update the book - see the confusion? are you saving a new book or updating one?
+HSET "some key" "edition" 2
+# to delete the book
+DEL "some key"
+```
+
+The above is so unrelated to the business logic that most of us 
+will take a number of minutes or hours trying to understand what 
+kind of data is even being saved. 
+
+Is it a book or some random stuff?
+
+Now consider something like this:
+
+```python
+book = Book(title="some title", edition=2, author=Author(name="Charles Payne", years_active=(1992, 2008)))
+store = Store(url="redis://localhost:6379/0", pool_size=5, default_ttl=3000, timeout=1)
+store.register_model(Book)
+
+Book.insert(data=book)
+response = Book.select(ids=["some title"])
+Book.update(_id="some title", data={"edition": 1})
+Book.delete(ids=["some title", "another title"])
+```
+
+Beautiful, isn't it?
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..bf3a8f6d
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,92 @@
+# Pydantic-redis
+
+A simple declarative ORM for redis based on pydantic
+
+## Features
+
+1. A subclass-able `Model` class to create Object Relational Mapping to redis hashes
+2. A redis `Store` class to mutate and query `Model`'s registered in it
+3. A `RedisConfig` class to pass to the `Store` constructor to connect to a redis instance
+4. A synchronous `syncio` and an asynchronous `asyncio` interface to the above classes
+
+### Installation
+<div class="termy">
+
+```console
+$ pip install pydantic-redis
+
+---> 100%
+```
+</div>
+
+### Synchronous Example
+
+#### Create it
+
+- Create a file `main.py` with:
+
+```Python
+{!../docs_src/index/sync_main.py!}
+```
+
+#### Run it
+
+Run the example with:
+
+<div class="termy">
+
+```console
+$ python main.py
+All:
+[   Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True),
+    Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False),
+    Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False),
+    Book(title='Great Expectations', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=5.0, published_on=datetime.date(1220, 4, 4), tags=['Classic'], in_stock=True)]
+
+Paginated:
+[   Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False),
+    Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True)]
+
+Paginated but with few fields:
+[   {   'author': Author(name='Charles Dickens', active_years=(1220, 1280)),
+        'in_stock': False},
+    {   'author': Author(name='Jane Austen', active_years=(1580, 1640)),
+        'in_stock': True}]
+```
+</div>
+
+### Asynchronous Example
+
+#### Create it
+
+- Create a file `main.py` with:
+
+```Python
+{!../docs_src/index/async_main.py!}
+```
+
+#### Run it
+
+Run the example with:
+
+<div class="termy">
+
+```console
+$ python main.py
+All:
+[   Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True),
+    Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False),
+    Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False),
+    Book(title='Great Expectations', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=5.0, published_on=datetime.date(1220, 4, 4), tags=['Classic'], in_stock=True)]
+
+Paginated:
+[   Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False),
+    Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True)]
+
+Paginated but with few fields:
+[   {   'author': Author(name='Charles Dickens', active_years=(1220, 1280)),
+        'in_stock': False},
+    {   'author': Author(name='Jane Austen', active_years=(1580, 1640)),
+        'in_stock': True}]
+```
+</div>
diff --git a/docs/js/custom.js b/docs/js/custom.js
new file mode 100644
index 00000000..1ac2a50c
--- /dev/null
+++ b/docs/js/custom.js
@@ -0,0 +1,114 @@
+function setupTermynal() {
+    document.querySelectorAll(".use-termynal").forEach(node => {
+        node.style.display = "block";
+        new Termynal(node, {
+            lineDelay: 500
+        });
+    });
+    const progressLiteralStart = "---> 100%";
+    const promptLiteralStart = "$ ";
+    const customPromptLiteralStart = "# ";
+    const termynalActivateClass = "termy";
+    let termynals = [];
+
+    function createTermynals() {
+        document
+            .querySelectorAll(`.${termynalActivateClass} .highlight`)
+            .forEach(node => {
+                const text = node.textContent;
+                const lines = text.split("\n");
+                const useLines = [];
+                let buffer = [];
+                function saveBuffer() {
+                    if (buffer.length) {
+                        let isBlankSpace = true;
+                        buffer.forEach(line => {
+                            if (line) {
+                                isBlankSpace = false;
+                            }
+                        });
+                        dataValue = {};
+                        if (isBlankSpace) {
+                            dataValue["delay"] = 0;
+                        }
+                        if (buffer[buffer.length - 1] === "") {
+                            // A last single <br> won't have effect
+                            // so put an additional one
+                            buffer.push("");
+                        }
+                        const bufferValue = buffer.join("<br>");
+                        dataValue["value"] = bufferValue;
+                        useLines.push(dataValue);
+                        buffer = [];
+                    }
+                }
+                for (let line of lines) {
+                    if (line === progressLiteralStart) {
+                        saveBuffer();
+                        useLines.push({
+                            type: "progress"
+                        });
+                    } else if (line.startsWith(promptLiteralStart)) {
+                        saveBuffer();
+                        const value = line.replace(promptLiteralStart, "").trimEnd();
+                        useLines.push({
+                            type: "input",
+                            value: value
+                        });
+                    } else if (line.startsWith("// ")) {
+                        saveBuffer();
+                        const value = "💬 " + line.replace("// ", "").trimEnd();
+                        useLines.push({
+                            value: value,
+                            class: "termynal-comment",
+                            delay: 0
+                        });
+                    } else if (line.startsWith(customPromptLiteralStart)) {
+                        saveBuffer();
+                        const promptStart = line.indexOf(promptLiteralStart);
+                        if (promptStart === -1) {
+                            console.error("Custom prompt found but no end delimiter", line)
+                        }
+                        const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "")
+                        let value = line.slice(promptStart + promptLiteralStart.length);
+                        useLines.push({
+                            type: "input",
+                            value: value,
+                            prompt: prompt
+                        });
+                    } else {
+                        buffer.push(line);
+                    }
+                }
+                saveBuffer();
+                const div = document.createElement("div");
+                node.replaceWith(div);
+                const termynal = new Termynal(div, {
+                    lineData: useLines,
+                    noInit: true,
+                    lineDelay: 500
+                });
+                termynals.push(termynal);
+            });
+    }
+
+    function loadVisibleTermynals() {
+        termynals = termynals.filter(termynal => {
+            if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {
+                termynal.init();
+                return false;
+            }
+            return true;
+        });
+    }
+    window.addEventListener("scroll", loadVisibleTermynals);
+    createTermynals();
+    loadVisibleTermynals();
+}
+
+
+async function main() {
+    setupTermynal();
+}
+
+main()
diff --git a/docs/js/termynal.js b/docs/js/termynal.js
new file mode 100644
index 00000000..4ac32708
--- /dev/null
+++ b/docs/js/termynal.js
@@ -0,0 +1,264 @@
+/**
+ * termynal.js
+ * A lightweight, modern and extensible animated terminal window, using
+ * async/await.
+ *
+ * @author Ines Montani <ines@ines.io>
+ * @version 0.0.1
+ * @license MIT
+ */
+
+'use strict';
+
+/** Generate a terminal widget. */
+class Termynal {
+    /**
+     * Construct the widget's settings.
+     * @param {(string|Node)=} container - Query selector or container element.
+     * @param {Object=} options - Custom settings.
+     * @param {string} options.prefix - Prefix to use for data attributes.
+     * @param {number} options.startDelay - Delay before animation, in ms.
+     * @param {number} options.typeDelay - Delay between each typed character, in ms.
+     * @param {number} options.lineDelay - Delay between each line, in ms.
+     * @param {number} options.progressLength - Number of characters displayed as progress bar.
+     * @param {string} options.progressChar – Character to use for progress bar, defaults to █.
+	 * @param {number} options.progressPercent - Max percent of progress.
+     * @param {string} options.cursor – Character to use for cursor, defaults to ▋.
+     * @param {Object[]} lineData - Dynamically loaded line data objects.
+     * @param {boolean} options.noInit - Don't initialise the animation.
+     */
+    constructor(container = '#termynal', options = {}) {
+        this.container = (typeof container === 'string') ? document.querySelector(container) : container;
+        this.pfx = `data-${options.prefix || 'ty'}`;
+        this.originalStartDelay = this.startDelay = options.startDelay
+            || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600;
+        this.originalTypeDelay = this.typeDelay = options.typeDelay
+            || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90;
+        this.originalLineDelay = this.lineDelay = options.lineDelay
+            || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500;
+        this.progressLength = options.progressLength
+            || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40;
+        this.progressChar = options.progressChar
+            || this.container.getAttribute(`${this.pfx}-progressChar`) || '█';
+		this.progressPercent = options.progressPercent
+            || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100;
+        this.cursor = options.cursor
+            || this.container.getAttribute(`${this.pfx}-cursor`) || '▋';
+        this.lineData = this.lineDataToElements(options.lineData || []);
+        this.loadLines()
+        if (!options.noInit) this.init()
+    }
+
+    loadLines() {
+        // Load all the lines and create the container so that the size is fixed
+        // Otherwise it would be changing and the user viewport would be constantly
+        // moving as she/he scrolls
+        const finish = this.generateFinish()
+        finish.style.visibility = 'hidden'
+        this.container.appendChild(finish)
+        // Appends dynamically loaded lines to existing line elements.
+        this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData);
+        for (let line of this.lines) {
+            line.style.visibility = 'hidden'
+            this.container.appendChild(line)
+        }
+        const restart = this.generateRestart()
+        restart.style.visibility = 'hidden'
+        this.container.appendChild(restart)
+        this.container.setAttribute('data-termynal', '');
+    }
+
+    /**
+     * Initialise the widget, get lines, clear container and start animation.
+     */
+    init() {
+        /**
+         * Calculates width and height of Termynal container.
+         * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
+         */
+        const containerStyle = getComputedStyle(this.container);
+        this.container.style.width = containerStyle.width !== '0px' ?
+            containerStyle.width : undefined;
+        this.container.style.minHeight = containerStyle.height !== '0px' ?
+            containerStyle.height : undefined;
+
+        this.container.setAttribute('data-termynal', '');
+        this.container.innerHTML = '';
+        for (let line of this.lines) {
+            line.style.visibility = 'visible'
+        }
+        this.start();
+    }
+
+    /**
+     * Start the animation and rener the lines depending on their data attributes.
+     */
+    async start() {
+        this.addFinish()
+        await this._wait(this.startDelay);
+
+        for (let line of this.lines) {
+            const type = line.getAttribute(this.pfx);
+            const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay;
+
+            if (type == 'input') {
+                line.setAttribute(`${this.pfx}-cursor`, this.cursor);
+                await this.type(line);
+                await this._wait(delay);
+            }
+
+            else if (type == 'progress') {
+                await this.progress(line);
+                await this._wait(delay);
+            }
+
+            else {
+                this.container.appendChild(line);
+                await this._wait(delay);
+            }
+
+            line.removeAttribute(`${this.pfx}-cursor`);
+        }
+        this.addRestart()
+        this.finishElement.style.visibility = 'hidden'
+        this.lineDelay = this.originalLineDelay
+        this.typeDelay = this.originalTypeDelay
+        this.startDelay = this.originalStartDelay
+    }
+
+    generateRestart() {
+        const restart = document.createElement('a')
+        restart.onclick = (e) => {
+            e.preventDefault()
+            this.container.innerHTML = ''
+            this.init()
+        }
+        restart.href = '#'
+        restart.setAttribute('data-terminal-control', '')
+        restart.innerHTML = "restart ↻"
+        return restart
+    }
+
+    generateFinish() {
+        const finish = document.createElement('a')
+        finish.onclick = (e) => {
+            e.preventDefault()
+            this.lineDelay = 0
+            this.typeDelay = 0
+            this.startDelay = 0
+        }
+        finish.href = '#'
+        finish.setAttribute('data-terminal-control', '')
+        finish.innerHTML = "fast →"
+        this.finishElement = finish
+        return finish
+    }
+
+    addRestart() {
+        const restart = this.generateRestart()
+        this.container.appendChild(restart)
+    }
+
+    addFinish() {
+        const finish = this.generateFinish()
+        this.container.appendChild(finish)
+    }
+
+    /**
+     * Animate a typed line.
+     * @param {Node} line - The line element to render.
+     */
+    async type(line) {
+        const chars = [...line.textContent];
+        line.textContent = '';
+        this.container.appendChild(line);
+
+        for (let char of chars) {
+            const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay;
+            await this._wait(delay);
+            line.textContent += char;
+        }
+    }
+
+    /**
+     * Animate a progress bar.
+     * @param {Node} line - The line element to render.
+     */
+    async progress(line) {
+        const progressLength = line.getAttribute(`${this.pfx}-progressLength`)
+            || this.progressLength;
+        const progressChar = line.getAttribute(`${this.pfx}-progressChar`)
+            || this.progressChar;
+        const chars = progressChar.repeat(progressLength);
+		const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`)
+			|| this.progressPercent;
+        line.textContent = '';
+        this.container.appendChild(line);
+
+        for (let i = 1; i < chars.length + 1; i++) {
+            await this._wait(this.typeDelay);
+            const percent = Math.round(i / chars.length * 100);
+            line.textContent = `${chars.slice(0, i)} ${percent}%`;
+			if (percent>progressPercent) {
+				break;
+			}
+        }
+    }
+
+    /**
+     * Helper function for animation delays, called with `await`.
+     * @param {number} time - Timeout, in ms.
+     */
+    _wait(time) {
+        return new Promise(resolve => setTimeout(resolve, time));
+    }
+
+    /**
+     * Converts line data objects into line elements.
+     *
+     * @param {Object[]} lineData - Dynamically loaded lines.
+     * @param {Object} line - Line data object.
+     * @returns {Element[]} - Array of line elements.
+     */
+    lineDataToElements(lineData) {
+        return lineData.map(line => {
+            let div = document.createElement('div');
+            div.innerHTML = `<span ${this._attributes(line)}>${line.value || ''}</span>`;
+
+            return div.firstElementChild;
+        });
+    }
+
+    /**
+     * Helper function for generating attributes string.
+     *
+     * @param {Object} line - Line data object.
+     * @returns {string} - String of attributes.
+     */
+    _attributes(line) {
+        let attrs = '';
+        for (let prop in line) {
+            // Custom add class
+            if (prop === 'class') {
+                attrs += ` class=${line[prop]} `
+                continue
+            }
+            if (prop === 'type') {
+                attrs += `${this.pfx}="${line[prop]}" `
+            } else if (prop !== 'value') {
+                attrs += `${this.pfx}-${prop}="${line[prop]}" `
+            }
+        }
+
+        return attrs;
+    }
+}
+
+/**
+* HTML API: If current script has container(s) specified, initialise Termynal.
+*/
+if (document.currentScript.hasAttribute('data-termynal-container')) {
+    const containers = document.currentScript.getAttribute('data-termynal-container');
+    containers.split('|')
+        .forEach(container => new Termynal(container))
+}
diff --git a/docs/reference.md b/docs/reference.md
new file mode 100644
index 00000000..8dcfa5cb
--- /dev/null
+++ b/docs/reference.md
@@ -0,0 +1,1 @@
+::: pydantic_redis
\ No newline at end of file
diff --git a/docs/tutorials/asynchronous/delete.md b/docs/tutorials/asynchronous/delete.md
new file mode 100644
index 00000000..088710dd
--- /dev/null
+++ b/docs/tutorials/asynchronous/delete.md
@@ -0,0 +1,48 @@
+# Delete
+
+Pydantic-redis can be used to delete model instances from redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6-9 18"
+{!../docs_src/tutorials/asynchronous/delete.py!}
+```
+
+## Delete Records
+
+To delete many records from redis, pass a list of primary keys (`ids`) of the records to the model's `delete` method.
+
+```Python hl_lines="30"
+{!../docs_src/tutorials/asynchronous/delete.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+pre-delete:
+[   Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Utah Blaine', author="Louis L'Amour"),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+post-delete:
+[   Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Utah Blaine', author="Louis L'Amour")]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/asynchronous/insert.md b/docs/tutorials/asynchronous/insert.md
new file mode 100644
index 00000000..c562feec
--- /dev/null
+++ b/docs/tutorials/asynchronous/insert.md
@@ -0,0 +1,87 @@
+# Insert
+
+Pydantic-redis can be used to insert new model instances into redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6-9 18"
+{!../docs_src/tutorials/asynchronous/insert.py!}
+```
+
+## Insert One Record
+
+To add a single record to the redis instance, pass that model's instance as first argument to the model's `insert` 
+method
+
+```Python hl_lines="20"
+{!../docs_src/tutorials/asynchronous/insert.py!}
+```
+
+## Insert One Record With TTL
+
+To make the record added to redis temporary, add a `life_span_seconds` (Time To Live i.e. TTL) key-word argument 
+when calling the model's `insert` method.
+
+!!! info
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used.
+    
+    The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default.
+
+```Python hl_lines="21-24"
+{!../docs_src/tutorials/asynchronous/insert.py!}
+```
+
+## Insert Many Records
+
+To add many records to the redis instance, pass a list of that model's instances as first argument to the model's
+`insert` method.
+
+!!! info
+    Adding many records at once is more performant than adding one record at a time repeatedly because less network requests
+    are made in the former.
+
+```Python hl_lines="25-30"
+{!../docs_src/tutorials/asynchronous/insert.py!}
+```
+
+## Insert Many Records With TTL
+
+To add temporary records to redis, add a `life_span_seconds` (Time To Live i.e. TTL) argument 
+when calling the model's `insert` method.
+
+!!! info
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used.
+    
+    The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default.
+
+```Python hl_lines="31-37"
+{!../docs_src/tutorials/asynchronous/insert.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+[   Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Great Expectations', author='Charles Dickens'),
+    Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/asynchronous/list-of-nested-models.md b/docs/tutorials/asynchronous/list-of-nested-models.md
new file mode 100644
index 00000000..c451edaf
--- /dev/null
+++ b/docs/tutorials/asynchronous/list-of-nested-models.md
@@ -0,0 +1,156 @@
+# Lists of Nested Models
+
+Sometimes, one might need to have models (schemas) that have lists of other models (schemas).
+
+An example is a `Folder` model that can have child `Folder`'s and `File`'s.
+
+This can easily be pulled off with pydantic-redis.
+
+## Import Pydantic-redis' `Model`
+
+First, import `pydantic-redis.asyncio`'s `Model`
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Create the Child Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="15-18"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Child Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two `File`'s with the same `path`.
+
+```Python hl_lines="16"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Create the Parent Model
+
+Next, declare another model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes, as before.
+
+```Python hl_lines="21-25"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Add the Nested Model List to the Parent Model
+
+Annotate the field that is to hold the child model list with the List of child class. 
+
+!!! example
+    In this case, the field `files` is annotated with `List[File]`.
+    
+    And the field `folders` is annotated with `"Folder"` class i.e. itself.
+
+```Python hl_lines="24-25"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Parent Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the parent Model.
+
+!!! example
+    In this case, there can be no two `Folder`'s with the same `path`.
+
+```Python hl_lines="22"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Register the Models in the Store
+
+Then, in order for the store to know the existence of each given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="36-37"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Use the Parent Model
+
+Then you can use the parent model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! info
+    The child models will be automatically inserted, or updated if they already exist
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="39-61"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Use the Child Model Independently
+
+You can also use the child model independently.
+
+!!! info
+    Any mutation on the child model will also be reflected in the any parent model instances 
+    fetched from redis after that mutation.
+
+```Python hl_lines="63-68"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Indirectly Update Child Model
+
+A child model can be indirectly updated via the parent model.
+
+Set the attribute containing the child model list with a list of instances of the child model 
+
+If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis.
+
+```Python hl_lines="63-66"
+{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+parent folder:
+[   Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=<FileType.TEXT: 'text'>), File(path='path/to/bar.jpg', type=<FileType.IMAGE: 'image'>)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=<FileType.TEXT: 'text'>), File(path='path/to/foo.jpg', type=<FileType.IMAGE: 'image'>)], folders=[])])]
+
+files:
+[   File(path='path/to/foo.txt', type=<FileType.TEXT: 'text'>),
+    File(path='path/to/foo.jpg', type=<FileType.IMAGE: 'image'>),
+    File(path='path/to/bar.txt', type=<FileType.TEXT: 'text'>),
+    File(path='path/to/bar.jpg', type=<FileType.IMAGE: 'image'>)]
+
+indirectly updated parent folder:
+[   Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=<FileType.TEXT: 'text'>), File(path='path/to/bar.jpg', type=<FileType.IMAGE: 'image'>)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=<FileType.EXEC: 'executable'>)], folders=[])])]
+
+indirectly updated files:
+[File(path='path/to/foo.txt', type=<FileType.EXEC: 'executable'>)]
+```
+</div>
diff --git a/docs/tutorials/asynchronous/models.md b/docs/tutorials/asynchronous/models.md
new file mode 100644
index 00000000..0ae6519e
--- /dev/null
+++ b/docs/tutorials/asynchronous/models.md
@@ -0,0 +1,80 @@
+# Models
+
+The very first thing you need to create for pydantic-redis are the models (or schemas) that
+the data you are to save in redis is to be based on.
+
+These models are derived from [pydantic's](https://docs.pydantic.dev/) `BaseModel`.
+
+## Import Pydantic-redis' `Model`
+
+First, import pydantic-redis.asyncio's `Model`
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6"
+{!../docs_src/tutorials/asynchronous/models.py!}
+```
+
+## Create the Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="9-16"
+{!../docs_src/tutorials/asynchronous/models.py!}
+```
+
+## Specify the `_primary_key_field` Attribute
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two books with the same `title`.
+
+```Python hl_lines="10"
+{!../docs_src/tutorials/asynchronous/models.py!}
+```
+
+## Register the Model in the Store
+
+Then, in order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="27"
+{!../docs_src/tutorials/asynchronous/models.py!}
+```
+
+## Use the Model
+
+Then you can use the model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="29-40"
+{!../docs_src/tutorials/asynchronous/models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+[   Book(title='Oliver Twist', author='Charles Dickens', rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/asynchronous/nested-models.md b/docs/tutorials/asynchronous/nested-models.md
new file mode 100644
index 00000000..9955beb6
--- /dev/null
+++ b/docs/tutorials/asynchronous/nested-models.md
@@ -0,0 +1,152 @@
+# Nested Models
+
+The very first thing you need to create for pydantic-redis are the models (or schemas) that
+the data you are to save in redis is to be based on.
+
+It is possible to refer one model in another model in a parent-child relationship.
+
+## Import Pydantic-redis' `Model`
+
+First, import `pydantic-redis.asyncio`'s `Model`.
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Create the Child Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="9-12"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Child Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two authors with the same `name`.
+
+```Python hl_lines="10"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Create the Parent Model
+
+Next, declare another model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes, as before.
+
+```Python hl_lines="15-22"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Add the Nested Model to the Parent Model
+
+Annotate the field that is to hold the child model with the child class. 
+
+!!! example
+    In this case, the field `author` is annotated with `Author` class.
+
+```Python hl_lines="18"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Parent Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the parent Model.
+
+!!! example
+    In this case, there can be no two books with the same `title`.
+
+```Python hl_lines="16"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Register the Models in the Store
+
+Then, in order for the store to know the existence of each given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="33-34"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Use the Parent Model
+
+Then you can use the parent model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! note
+    The child model will be automatically inserted, or updated if it already exists
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="36-48"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Use the Child Model Independently
+
+You can also use the child model independently.
+
+!!! info
+    Any mutation on the child model will also be reflected in the any parent model instances 
+    fetched from redis after that mutation.
+
+```Python hl_lines="50-51"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Indirectly Update Child Model
+
+A child model can be indirectly updated via the parent model.
+
+Set the attribute containing the child model with an instance of the child model
+
+!!! note
+    The new instance of the child model should have the **SAME** primary key as the original
+    child model.
+
+```Python hl_lines="53-57"
+{!../docs_src/tutorials/asynchronous/nested-models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+book:
+[   Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1999, 2007)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)]
+
+author:
+[Author(name='Charles Dickens', active_years=(1999, 2007))]
+
+indirectly updated book:
+[   Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1227, 1277)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)]
+
+indirectly updated author:
+[Author(name='Charles Dickens', active_years=(1969, 1999))]
+```
+</div>
diff --git a/docs/tutorials/asynchronous/select.md b/docs/tutorials/asynchronous/select.md
new file mode 100644
index 00000000..7d5c7d58
--- /dev/null
+++ b/docs/tutorials/asynchronous/select.md
@@ -0,0 +1,125 @@
+# Select
+
+Pydantic-redis can be used to retrieve model instances from redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`.
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6-9 18"
+{!../docs_src/tutorials/asynchronous/select-records.py!}
+```
+
+## Select All Records
+
+To select all records for the given model in redis, call the model's `select` method without any arguments.
+
+```Python hl_lines="29"
+{!../docs_src/tutorials/asynchronous/select-records.py!}
+```
+
+## Select Some Fields for All Records
+
+To select some fields for all records for the given model in redis, pass the desired fields (`columns`) to the model's 
+`select` method.
+
+!!! info
+    This returns dictionaries instead of Model instances.
+
+```Python hl_lines="34"
+{!../docs_src/tutorials/asynchronous/select-records.py!}
+```
+
+## Select Some Records
+
+To select some records for the given model in redis, pass a list of the primary keys (`ids`) of the desired records to 
+the model's `select` method.
+
+```Python hl_lines="30-32"
+{!../docs_src/tutorials/asynchronous/select-records.py!}
+```
+
+## Select Some Fields for Some Records
+
+We can go further and limit the fields returned for the desired records.
+
+We pass the desired fields (`columns`) to the model's `select` method, together with the list of the primary keys 
+(`ids`) of the desired records.
+
+!!! info
+    This returns dictionaries instead of Model instances.
+
+```Python hl_lines="35-37"
+{!../docs_src/tutorials/asynchronous/select-records.py!}
+```
+
+## Select Records Page by Page 
+
+In order to avoid overwhelming the server's memory resources, we can get the records one page at a time i.e. pagination.
+
+We do this by specifying the number of records per page (`limit`) and the number of records to skip (`skip`) 
+when calling the model's `select` method
+
+!!! info
+    Records are ordered by timestamp of their insert into redis. 
+    
+    For batch inserts, the time difference is quite small but consistent. 
+
+!!! tip
+    You don't have to pass the `skip` if you wish to get the first records. `skip` defaults to 0.
+    
+    `limit`, however is mandatory.
+
+!!! warning
+    When both `ids` and `limit` are supplied, pagination is ignored. 
+
+    It wouldn't make any sense otherwise.
+
+```Python hl_lines="39-42"
+{!../docs_src/tutorials/asynchronous/select-records.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+all:
+[   Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Utah Blaine', author="Louis L'Amour"),
+    Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+by id:
+[   Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+some fields for all:
+[   {'author': 'Charles Dickens'},
+    {'author': "Louis L'Amour"},
+    {'author': 'Emily Bronte'},
+    {'author': 'Jane Austen'}]
+
+some fields for given ids:
+[{'author': 'Charles Dickens'}, {'author': 'Jane Austen'}]
+
+paginated; skip: 0, limit: 2:
+[   Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Jane Eyre', author='Emily Bronte')]
+
+paginated returning some fields for each; skip: 2, limit: 2:
+[{'author': 'Jane Austen'}, {'author': "Louis L'Amour"}]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/asynchronous/tuple-of-nested-models.md b/docs/tutorials/asynchronous/tuple-of-nested-models.md
new file mode 100644
index 00000000..27bef053
--- /dev/null
+++ b/docs/tutorials/asynchronous/tuple-of-nested-models.md
@@ -0,0 +1,154 @@
+# Tuples of Nested Models
+
+Sometimes, one might need to have models (schemas) that have tuples of other models (schemas).
+
+An example is a `ScoreBoard` model that can have Tuples of player name and `Scores`'.
+
+This can easily be pulled off with pydantic-redis.
+
+## Import Pydantic-redis' `Model`
+
+First, import `pydantic-redis.asyncio`'s `Model`.
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="4"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Create the Child Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="7-10"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Child Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two `Score`'s with the same `id`.
+
+```Python hl_lines="8"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Create the Parent Model
+
+Next, declare another model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes, as before.
+
+```Python hl_lines="13-16"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Add the Nested Model Tuple to the Parent Model
+
+Annotate the field that is to hold the tuple of child models with the Tuple of child class. 
+
+!!! example
+    In this case, the field `scores` is annotated with `Tuple[str, Score]` class.
+
+!!! info
+    The `str` is the player's name.
+
+```Python hl_lines="16"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Parent Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the parent Model.
+
+!!! example
+    In this case, there can be no two `ScoreBoard`'s with the same `id`.
+
+```Python hl_lines="14"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Register the Models in the Store
+
+Then, in order for the store to know the existence of each given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="23-24"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Use the Parent Model
+
+Then you can use the parent model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! info
+    The child models will be automatically inserted, or updated if they already exist
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="26-36"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Use the Child Model Independently
+
+You can also use the child model independently.
+
+!!! info
+    Any mutation on the child model will also be reflected in the any parent model instances 
+    fetched from redis after that mutation.
+
+```Python hl_lines="38-39"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Indirectly Update Child Model
+
+A child model can be indirectly updated via the parent model.
+
+Set the attribute containing the child model tuple with a tuple of instances of the child model 
+
+If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis.
+
+```Python hl_lines="41-50"
+{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+score board:
+[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=50)))]
+
+scores:
+[Score(id='some id', total=50)]
+
+indirectly updated score board:
+[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=78)))]
+
+indirectly updated score:
+[Score(id='some id', total=60)]
+```
+</div>
diff --git a/docs/tutorials/asynchronous/update.md b/docs/tutorials/asynchronous/update.md
new file mode 100644
index 00000000..b004c459
--- /dev/null
+++ b/docs/tutorials/asynchronous/update.md
@@ -0,0 +1,86 @@
+# Update
+
+Pydantic-redis can be used to update model instances in redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`.
+
+!!! warning
+    The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6-9 18"
+{!../docs_src/tutorials/asynchronous/update.py!}
+```
+
+## Update One Record
+
+To update a single record in redis, pass the primary key (`_id`) of that record and the new changes to the model's `update` 
+method
+
+```Python hl_lines="27"
+{!../docs_src/tutorials/asynchronous/update.py!}
+```
+
+## Update One Record With TTL
+
+To update the record's time-to-live (TTL) also, pass the `life_span_seconds` argument to the model's `update` method.
+
+!!! info
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used.
+    
+    The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default.
+
+```Python hl_lines="28-30"
+{!../docs_src/tutorials/asynchronous/update.py!}
+```
+
+## Update/Upsert Many Records
+
+To update many records in redis, pass a list of that model's instances as first argument to the model's
+`insert` method.
+
+Technically, this will insert any records that don't exist and overwrite any that exist already.
+
+!!! info
+    Updating many records at once is more performant than adding one record at a time repeatedly because less network requests
+    are made in the former.
+
+!!! warning
+    Calling `insert` always overwrites the time-to-live of the records updated. 
+
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used. 
+
+    By default `life_span_seconds` is `None` i.e. the time-to-live is removed and the updated records never expire.
+
+```Python hl_lines="33-40"
+{!../docs_src/tutorials/asynchronous/update.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+single update:
+[   Book(title='Jane Eyre', author='Daniel McKenzie'),
+    Book(title='Oliver Twist', author='Charlie Ickens'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+multi update:
+[   Book(title='Jane Eyre', author='Emiliano Bronte'),
+    Book(title='Oliver Twist', author='Chuck Dickens'),
+    Book(title='Pride and Prejudice', author='Janey Austen')]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/intro.md b/docs/tutorials/intro.md
new file mode 100644
index 00000000..1eeda945
--- /dev/null
+++ b/docs/tutorials/intro.md
@@ -0,0 +1,65 @@
+# Intro
+
+This tutorial shows you how to use **pydantic-redis** step by step.
+
+There are two tutorials: Synchronous API and Asynchronous API.
+
+In either tutorials, each child section gradually builds on the previous one. These child sections are separate topics
+so that one can go directly to a specific topic, just like a reference.
+
+## Synchronous API
+
+In case you are looking to use pydantic-redis without async/await, you can read the **Synchronous API** version of this
+tutorial.
+
+!!! info
+    This is the default API for pydantic-redis.
+
+## Asynchronous API
+
+In case you are looking to use pydantic-redis with async/await, e.g. in [FastAPI](https://fastapi.tiangolo.com)
+or [asyncio](https://docs.python.org/3/library/asyncio.html) , you can read the **Asynchronous API** version of this
+tutorial.
+
+## Install Python
+
+Pydantic-redis requires python 3.6 and above. The latest stable python version is the recommended version.
+
+You can install python from [the official python downloads site](https://www.python.org/downloads/).
+
+## Install Redis
+
+In order to use pydantic-redis, you need a redis server instance running. You can install a local instance
+via [the official redis stack](https://redis.io/docs/stack/get-started/install/) instructions.
+
+!!! info
+    You may also need a visual client to view the data in redis. The recommended app to use
+    is [RedisInsight](https://redis.com/redis-enterprise/redis-insight/).
+
+## Run the Code
+
+All the code blocks can be copied and used directly.
+
+To run any of the examples, copy the code to a file `main.py`, and run the command below in your terminal:
+
+<div class="termy">
+
+```console
+$ python main.py
+```
+
+</div>
+
+## Install Pydantic-redis
+
+First install pydantic-redis
+
+<div class="termy">
+
+```console
+$ pip install pydantic-redis
+
+---> 100%
+```
+
+</div>
diff --git a/docs/tutorials/synchronous/delete.md b/docs/tutorials/synchronous/delete.md
new file mode 100644
index 00000000..1fbacb28
--- /dev/null
+++ b/docs/tutorials/synchronous/delete.md
@@ -0,0 +1,45 @@
+# Delete
+
+Pydantic-redis can be used to delete model instances from redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="5-8 17"
+{!../docs_src/tutorials/synchronous/delete.py!}
+```
+
+## Delete Records
+
+To delete many records from redis, pass a list of primary keys (`ids`) of the records to the model's `delete` method.
+
+```Python hl_lines="29"
+{!../docs_src/tutorials/synchronous/delete.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+pre-delete:
+[   Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Utah Blaine', author="Louis L'Amour"),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+post-delete:
+[   Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Utah Blaine', author="Louis L'Amour")]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/synchronous/insert.md b/docs/tutorials/synchronous/insert.md
new file mode 100644
index 00000000..e62dd706
--- /dev/null
+++ b/docs/tutorials/synchronous/insert.md
@@ -0,0 +1,84 @@
+# Insert
+
+Pydantic-redis can be used to insert new model instances into redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="5-8 17"
+{!../docs_src/tutorials/synchronous/insert.py!}
+```
+
+## Insert One Record
+
+To add a single record to the redis instance, pass that model's instance as first argument to the model's `insert` 
+method
+
+```Python hl_lines="19"
+{!../docs_src/tutorials/synchronous/insert.py!}
+```
+
+## Insert One Record With TTL
+
+To make the record added to redis temporary, add a `life_span_seconds` (Time To Live i.e. TTL) key-word argument 
+when calling the model's `insert` method.
+
+!!! info
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used.
+    
+    The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default.
+
+```Python hl_lines="20-23"
+{!../docs_src/tutorials/synchronous/insert.py!}
+```
+
+## Insert Many Records
+
+To add many records to the redis instance, pass a list of that model's instances as first argument to the model's
+`insert` method.
+
+!!! info
+    Adding many records at once is more performant than adding one record at a time repeatedly because less network requests
+    are made in the former.
+
+```Python hl_lines="24-29"
+{!../docs_src/tutorials/synchronous/insert.py!}
+```
+
+## Insert Many Records With TTL
+
+To add temporary records to redis, add a `life_span_seconds` (Time To Live i.e. TTL) argument 
+when calling the model's `insert` method.
+
+!!! info
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used.
+    
+    The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default.
+
+```Python hl_lines="30-36"
+{!../docs_src/tutorials/synchronous/insert.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+[   Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Great Expectations', author='Charles Dickens'),
+    Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/synchronous/list-of-nested-models.md b/docs/tutorials/synchronous/list-of-nested-models.md
new file mode 100644
index 00000000..4a1d82c5
--- /dev/null
+++ b/docs/tutorials/synchronous/list-of-nested-models.md
@@ -0,0 +1,153 @@
+# Lists of Nested Models
+
+Sometimes, one might need to have models (schemas) that have lists of other models (schemas).
+
+An example is a `Folder` model that can have child `Folder`'s and `File`'s.
+
+This can easily be pulled off with pydantic-redis.
+
+## Import Pydantic-redis' `Model`
+
+First, import pydantic-redis' `Model`
+
+```Python hl_lines="5"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Create the Child Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="14-17"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Child Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two `File`'s with the same `path`.
+
+```Python hl_lines="15"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Create the Parent Model
+
+Next, declare another model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes, as before.
+
+```Python hl_lines="20-24"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Add the Nested Model List to the Parent Model
+
+Annotate the field that is to hold the child model list with the List of child class. 
+
+!!! example
+    In this case, the field `files` is annotated with `List[File]`.
+    
+    And the field `folders` is annotated with `"Folder"` class i.e. itself.
+
+```Python hl_lines="23-24"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Parent Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the parent Model.
+
+!!! example
+    In this case, there can be no two `Folder`'s with the same `path`.
+
+```Python hl_lines="21"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Register the Models in the Store
+
+Then, in order for the store to know the existence of each given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="35-36"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Use the Parent Model
+
+Then you can use the parent model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! info
+    The child models will be automatically inserted, or updated if they already exist
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="38-60"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Use the Child Model Independently
+
+You can also use the child model independently.
+
+!!! info
+    Any mutation on the child model will also be reflected in the any parent model instances 
+    fetched from redis after that mutation.
+
+```Python hl_lines="62-67"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Indirectly Update Child Model
+
+A child model can be indirectly updated via the parent model.
+
+Set the attribute containing the child model list with a list of instances of the child model 
+
+If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis.
+
+```Python hl_lines="62-65"
+{!../docs_src/tutorials/synchronous/list-of-nested-models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+parent folder:
+[   Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=<FileType.TEXT: 'text'>), File(path='path/to/bar.jpg', type=<FileType.IMAGE: 'image'>)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=<FileType.TEXT: 'text'>), File(path='path/to/foo.jpg', type=<FileType.IMAGE: 'image'>)], folders=[])])]
+
+files:
+[   File(path='path/to/foo.txt', type=<FileType.TEXT: 'text'>),
+    File(path='path/to/foo.jpg', type=<FileType.IMAGE: 'image'>),
+    File(path='path/to/bar.txt', type=<FileType.TEXT: 'text'>),
+    File(path='path/to/bar.jpg', type=<FileType.IMAGE: 'image'>)]
+
+indirectly updated parent folder:
+[   Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=<FileType.TEXT: 'text'>), File(path='path/to/bar.jpg', type=<FileType.IMAGE: 'image'>)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=<FileType.EXEC: 'executable'>)], folders=[])])]
+
+indirectly updated files:
+[File(path='path/to/foo.txt', type=<FileType.EXEC: 'executable'>)]
+```
+</div>
diff --git a/docs/tutorials/synchronous/models.md b/docs/tutorials/synchronous/models.md
new file mode 100644
index 00000000..4635b4c4
--- /dev/null
+++ b/docs/tutorials/synchronous/models.md
@@ -0,0 +1,77 @@
+# Models
+
+The very first thing you need to create for pydantic-redis are the models (or schemas) that
+the data you are to save in redis is to be based on.
+
+These models are derived from [pydantic's](https://docs.pydantic.dev/) `BaseModel`.
+
+## Import Pydantic-redis' `Model`
+
+First, import pydantic-redis' `Model`
+
+```Python hl_lines="5"
+{!../docs_src/tutorials/synchronous/models.py!}
+```
+
+## Create the Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="8-15"
+{!../docs_src/tutorials/synchronous/models.py!}
+```
+
+## Specify the `_primary_key_field` Attribute
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two books with the same `title`.
+
+```Python hl_lines="9"
+{!../docs_src/tutorials/synchronous/models.py!}
+```
+
+## Register the Model in the Store
+
+Then, in order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="26"
+{!../docs_src/tutorials/synchronous/models.py!}
+```
+
+## Use the Model
+
+Then you can use the model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="28-39"
+{!../docs_src/tutorials/synchronous/models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+[   Book(title='Oliver Twist', author='Charles Dickens', rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/synchronous/nested-models.md b/docs/tutorials/synchronous/nested-models.md
new file mode 100644
index 00000000..c6416fc4
--- /dev/null
+++ b/docs/tutorials/synchronous/nested-models.md
@@ -0,0 +1,149 @@
+# Nested Models
+
+The very first thing you need to create for pydantic-redis are the models (or schemas) that
+the data you are to save in redis is to be based on.
+
+It is possible to refer one model in another model in a parent-child relationship.
+
+## Import Pydantic-redis' `Model`
+
+First, import pydantic-redis' `Model`
+
+```Python hl_lines="5"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Create the Child Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="8-11"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Child Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two authors with the same `name`.
+
+```Python hl_lines="9"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Create the Parent Model
+
+Next, declare another model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes, as before.
+
+```Python hl_lines="14-21"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Add the Nested Model to the Parent Model
+
+Annotate the field that is to hold the child model with the child class. 
+
+!!! example
+    In this case, the field `author` is annotated with `Author` class.
+
+```Python hl_lines="17"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Parent Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the parent Model.
+
+!!! example
+    In this case, there can be no two books with the same `title`.
+
+```Python hl_lines="15"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Register the Models in the Store
+
+Then, in order for the store to know the existence of each given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="32-33"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Use the Parent Model
+
+Then you can use the parent model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! note
+    The child model will be automatically inserted, or updated if it already exists
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="35-47"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Use the Child Model Independently
+
+You can also use the child model independently.
+
+!!! info
+    Any mutation on the child model will also be reflected in the any parent model instances 
+    fetched from redis after that mutation.
+
+```Python hl_lines="49-50"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Indirectly Update Child Model
+
+A child model can be indirectly updated via the parent model.
+
+Set the attribute containing the child model with an instance of the child model
+
+!!! note
+    The new instance of the child model should have the **SAME** primary key as the original
+    child model.
+
+```Python hl_lines="52-56"
+{!../docs_src/tutorials/synchronous/nested-models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+book:
+[   Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1999, 2007)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)]
+
+author:
+[Author(name='Charles Dickens', active_years=(1999, 2007))]
+
+indirectly updated book:
+[   Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1227, 1277)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)]
+
+indirectly updated author:
+[Author(name='Charles Dickens', active_years=(1969, 1999))]
+```
+</div>
diff --git a/docs/tutorials/synchronous/select.md b/docs/tutorials/synchronous/select.md
new file mode 100644
index 00000000..f8ca4883
--- /dev/null
+++ b/docs/tutorials/synchronous/select.md
@@ -0,0 +1,122 @@
+# Select
+
+Pydantic-redis can be used to retrieve model instances from redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="5-8 17"
+{!../docs_src/tutorials/synchronous/select-records.py!}
+```
+
+## Select All Records
+
+To select all records for the given model in redis, call the model's `select` method without any arguments.
+
+```Python hl_lines="28"
+{!../docs_src/tutorials/synchronous/select-records.py!}
+```
+
+## Select Some Fields for All Records
+
+To select some fields for all records for the given model in redis, pass the desired fields (`columns`) to the model's 
+`select` method.
+
+!!! info
+    This returns dictionaries instead of Model instances.
+
+```Python hl_lines="31"
+{!../docs_src/tutorials/synchronous/select-records.py!}
+```
+
+## Select Some Records
+
+To select some records for the given model in redis, pass a list of the primary keys (`ids`) of the desired records to 
+the model's `select` method.
+
+```Python hl_lines="29"
+{!../docs_src/tutorials/synchronous/select-records.py!}
+```
+
+## Select Some Fields for Some Records
+
+We can go further and limit the fields returned for the desired records.
+
+We pass the desired fields (`columns`) to the model's `select` method, together with the list of the primary keys 
+(`ids`) of the desired records.
+
+!!! info
+    This returns dictionaries instead of Model instances.
+
+```Python hl_lines="32-34"
+{!../docs_src/tutorials/synchronous/select-records.py!}
+```
+
+## Select Records Page by Page 
+
+In order to avoid overwhelming the server's memory resources, we can get the records one page at a time i.e. pagination.
+
+We do this by specifying the number of records per page (`limit`) and the number of records to skip (`skip`) 
+when calling the model's `select` method
+
+!!! info
+    Records are ordered by timestamp of their insert into redis. 
+    
+    For batch inserts, the time difference is quite small but consistent. 
+
+!!! tip
+    You don't have to pass the `skip` if you wish to get the first records. `skip` defaults to 0.
+    
+    `limit`, however is mandatory.
+
+!!! warning
+    When both `ids` and `limit` are supplied, pagination is ignored. 
+
+    It wouldn't make any sense otherwise.
+
+```Python hl_lines="36-39"
+{!../docs_src/tutorials/synchronous/select-records.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+all:
+[   Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Utah Blaine', author="Louis L'Amour"),
+    Book(title='Jane Eyre', author='Emily Bronte'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+by id:
+[   Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+some fields for all:
+[   {'author': 'Charles Dickens'},
+    {'author': "Louis L'Amour"},
+    {'author': 'Emily Bronte'},
+    {'author': 'Jane Austen'}]
+
+some fields for given ids:
+[{'author': 'Charles Dickens'}, {'author': 'Jane Austen'}]
+
+paginated; skip: 0, limit: 2:
+[   Book(title='Oliver Twist', author='Charles Dickens'),
+    Book(title='Jane Eyre', author='Emily Bronte')]
+
+paginated returning some fields for each; skip: 2, limit: 2:
+[{'author': 'Jane Austen'}, {'author': "Louis L'Amour"}]
+```
+</div>
\ No newline at end of file
diff --git a/docs/tutorials/synchronous/tuple-of-nested-models.md b/docs/tutorials/synchronous/tuple-of-nested-models.md
new file mode 100644
index 00000000..45210fa2
--- /dev/null
+++ b/docs/tutorials/synchronous/tuple-of-nested-models.md
@@ -0,0 +1,151 @@
+# Tuples of Nested Models
+
+Sometimes, one might need to have models (schemas) that have tuples of other models (schemas).
+
+An example is a `ScoreBoard` model that can have Tuples of player name and `Scores`'.
+
+This can easily be pulled off with pydantic-redis.
+
+## Import Pydantic-redis' `Model`
+
+First, import pydantic-redis' `Model`
+
+```Python hl_lines="3"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Create the Child Model
+
+Next, declare a new model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes.
+
+```Python hl_lines="6-9"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Child Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the Model.
+
+!!! example
+    In this case, there can be no two `Score`'s with the same `id`.
+
+```Python hl_lines="7"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Create the Parent Model
+
+Next, declare another model as a class that inherits from `Model`.
+
+Use standard Python types for all attributes, as before.
+
+```Python hl_lines="12-15"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Add the Nested Model Tuple to the Parent Model
+
+Annotate the field that is to hold the tuple of child models with the Tuple of child class. 
+
+!!! example
+    In this case, the field `scores` is annotated with `Tuple[str, Score]` class.
+
+!!! info
+    The `str` is the player's name.
+
+```Python hl_lines="15"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Set the `_primary_key_field` of the Parent Model
+
+Set the `_primary_key_field` attribute to the name of the attribute
+that is to act as a unique identifier for each instance of the parent Model.
+
+!!! example
+    In this case, there can be no two `ScoreBoard`'s with the same `id`.
+
+```Python hl_lines="13"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Register the Models in the Store
+
+Then, in order for the store to know the existence of each given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="22-23"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Use the Parent Model
+
+Then you can use the parent model class to:
+
+- `insert` into the store
+- `update` an instance of the model
+- `delete` from store
+- `select` from store
+
+!!! info
+    The child models will be automatically inserted, or updated if they already exist
+
+!!! info
+    The store is connected to the Redis instance, so any changes you make will
+    reflect in redis itself.
+
+```Python hl_lines="25-35"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Use the Child Model Independently
+
+You can also use the child model independently.
+
+!!! info
+    Any mutation on the child model will also be reflected in the any parent model instances 
+    fetched from redis after that mutation.
+
+```Python hl_lines="37-38"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Indirectly Update Child Model
+
+A child model can be indirectly updated via the parent model.
+
+Set the attribute containing the child model tuple with a tuple of instances of the child model 
+
+If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis.
+
+```Python hl_lines="40-49"
+{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+score board:
+[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=50)))]
+
+scores:
+[Score(id='some id', total=50)]
+
+indirectly updated score board:
+[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=78)))]
+
+indirectly updated score:
+[Score(id='some id', total=60)]
+```
+</div>
diff --git a/docs/tutorials/synchronous/update.md b/docs/tutorials/synchronous/update.md
new file mode 100644
index 00000000..c8571a71
--- /dev/null
+++ b/docs/tutorials/synchronous/update.md
@@ -0,0 +1,83 @@
+# Update
+
+Pydantic-redis can be used to update model instances in redis.
+
+## Create and register the Model
+
+A model is a class that inherits from `Model` with its `_primary_key_field` attribute set.
+
+In order for the store to know the existence of the given model, 
+register it using the `register_model` method of `Store`
+
+```Python hl_lines="5-8 17"
+{!../docs_src/tutorials/synchronous/update.py!}
+```
+
+## Update One Record
+
+To update a single record in redis, pass the primary key (`_id`) of that record and the new changes to the model's `update` 
+method
+
+```Python hl_lines="26"
+{!../docs_src/tutorials/synchronous/update.py!}
+```
+
+## Update One Record With TTL
+
+To update the record's time-to-live (TTL) also, pass the `life_span_seconds` argument to the model's `update` method.
+
+!!! info
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used.
+    
+    The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default.
+
+```Python hl_lines="27-29"
+{!../docs_src/tutorials/synchronous/update.py!}
+```
+
+## Update/Upsert Many Records
+
+To update many records in redis, pass a list of that model's instances as first argument to the model's
+`insert` method.
+
+Technically, this will insert any records that don't exist and overwrite any that exist already.
+
+!!! info
+    Updating many records at once is more performant than adding one record at a time repeatedly because less network requests
+    are made in the former.
+
+!!! warning
+    Calling `insert` always overwrites the time-to-live of the records updated. 
+
+    When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during
+    initialization is used. 
+
+    By default `life_span_seconds` is `None` i.e. the time-to-live is removed and the updated records never expire.
+
+```Python hl_lines="32-39"
+{!../docs_src/tutorials/synchronous/update.py!}
+```
+
+## Run the App
+
+Running the above code in a file `main.py` would produce:
+
+!!! tip
+    Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
+
+<div class="termy">
+
+```console
+$ python main.py
+single update:
+[   Book(title='Jane Eyre', author='Daniel McKenzie'),
+    Book(title='Oliver Twist', author='Charlie Ickens'),
+    Book(title='Pride and Prejudice', author='Jane Austen')]
+
+multi update:
+[   Book(title='Jane Eyre', author='Emiliano Bronte'),
+    Book(title='Oliver Twist', author='Chuck Dickens'),
+    Book(title='Pride and Prejudice', author='Janey Austen')]
+```
+</div>
\ No newline at end of file
diff --git a/docs_src/index/async_main.py b/docs_src/index/async_main.py
new file mode 100644
index 00000000..e553776e
--- /dev/null
+++ b/docs_src/index/async_main.py
@@ -0,0 +1,89 @@
+import asyncio
+import pprint
+from datetime import date
+from typing import Tuple, List
+from pydantic_redis.asyncio import RedisConfig, Model, Store
+
+
+class Author(Model):
+    _primary_key_field: str = "name"
+    name: str
+    active_years: Tuple[int, int]
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: Author
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+async def run_async():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(Book)
+    store.register_model(Author)
+
+    authors = {
+        "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
+        "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
+    }
+
+    books = [
+        Book(
+            title="Oliver Twist",
+            author=authors["charles"],
+            published_on=date(year=1215, month=4, day=4),
+            in_stock=False,
+            rating=2,
+            tags=["Classic"],
+        ),
+        Book(
+            title="Great Expectations",
+            author=authors["charles"],
+            published_on=date(year=1220, month=4, day=4),
+            rating=5,
+            tags=["Classic"],
+        ),
+        Book(
+            title="Jane Eyre",
+            author=authors["charles"],
+            published_on=date(year=1225, month=6, day=4),
+            in_stock=False,
+            rating=3.4,
+            tags=["Classic", "Romance"],
+        ),
+        Book(
+            title="Wuthering Heights",
+            author=authors["jane"],
+            published_on=date(year=1600, month=4, day=4),
+            rating=4.0,
+            tags=["Classic", "Romance"],
+        ),
+    ]
+
+    await Book.insert(books, life_span_seconds=3600)
+    all_books = await Book.select()
+    paginated_books = await Book.select(skip=2, limit=2)
+    paginated_books_with_few_fields = await Book.select(
+        columns=["author", "in_stock"], skip=2, limit=2
+    )
+    print("All:")
+    pp.pprint(all_books)
+    print("\nPaginated:")
+    pp.pprint(paginated_books)
+    print("\nPaginated but with few fields:")
+    pp.pprint(paginated_books_with_few_fields)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(run_async())
diff --git a/docs_src/index/sync_main.py b/docs_src/index/sync_main.py
new file mode 100644
index 00000000..f83df7a7
--- /dev/null
+++ b/docs_src/index/sync_main.py
@@ -0,0 +1,83 @@
+import pprint
+from datetime import date
+from typing import Tuple, List
+from pydantic_redis import RedisConfig, Model, Store
+
+
+class Author(Model):
+    _primary_key_field: str = "name"
+    name: str
+    active_years: Tuple[int, int]
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: Author
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(Book)
+    store.register_model(Author)
+
+    authors = {
+        "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
+        "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
+    }
+
+    books = [
+        Book(
+            title="Oliver Twist",
+            author=authors["charles"],
+            published_on=date(year=1215, month=4, day=4),
+            in_stock=False,
+            rating=2,
+            tags=["Classic"],
+        ),
+        Book(
+            title="Great Expectations",
+            author=authors["charles"],
+            published_on=date(year=1220, month=4, day=4),
+            rating=5,
+            tags=["Classic"],
+        ),
+        Book(
+            title="Jane Eyre",
+            author=authors["charles"],
+            published_on=date(year=1225, month=6, day=4),
+            in_stock=False,
+            rating=3.4,
+            tags=["Classic", "Romance"],
+        ),
+        Book(
+            title="Wuthering Heights",
+            author=authors["jane"],
+            published_on=date(year=1600, month=4, day=4),
+            rating=4.0,
+            tags=["Classic", "Romance"],
+        ),
+    ]
+
+    Book.insert(books, life_span_seconds=3600)
+    all_books = Book.select()
+    paginated_books = Book.select(skip=2, limit=2)
+    paginated_books_with_few_fields = Book.select(
+        columns=["author", "in_stock"], skip=2, limit=2
+    )
+    print("All:")
+    pp.pprint(all_books)
+    print("\nPaginated:")
+    pp.pprint(paginated_books)
+    print("\nPaginated but with few fields:")
+    pp.pprint(paginated_books_with_few_fields)
diff --git a/docs_src/tutorials/asynchronous/delete.py b/docs_src/tutorials/asynchronous/delete.py
new file mode 100644
index 00000000..2ffef33e
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/delete.py
@@ -0,0 +1,42 @@
+import asyncio
+import pprint
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    await Book.insert(
+        [
+            Book(title="Oliver Twist", author="Charles Dickens"),
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+            Book(title="Utah Blaine", author="Louis L'Amour"),
+        ]
+    )
+    pre_delete_response = await Book.select()
+
+    await Book.delete(ids=["Oliver Twist", "Pride and Prejudice"])
+    post_delete_response = await Book.select()
+
+    print("pre-delete:")
+    pp.pprint(pre_delete_response)
+
+    print("\npost-delete:")
+    pp.pprint(post_delete_response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/insert.py b/docs_src/tutorials/asynchronous/insert.py
new file mode 100644
index 00000000..8690b2fd
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/insert.py
@@ -0,0 +1,45 @@
+import asyncio
+import pprint
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    await Book.insert(Book(title="Oliver Twist", author="Charles Dickens"))
+    await Book.insert(
+        Book(title="Great Expectations", author="Charles Dickens"),
+        life_span_seconds=1800,
+    )
+    await Book.insert(
+        [
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+        ]
+    )
+    await Book.insert(
+        [
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+        ],
+        life_span_seconds=3600,
+    )
+
+    response = await Book.select()
+    pp.pprint(response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/list-of-nested-models.py b/docs_src/tutorials/asynchronous/list-of-nested-models.py
new file mode 100644
index 00000000..e5c53497
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/list-of-nested-models.py
@@ -0,0 +1,83 @@
+import asyncio
+import pprint
+from enum import Enum
+from typing import List
+
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class FileType(Enum):
+    TEXT = "text"
+    IMAGE = "image"
+    EXEC = "executable"
+
+
+class File(Model):
+    _primary_key_field: str = "path"
+    path: str
+    type: FileType
+
+
+class Folder(Model):
+    _primary_key_field: str = "path"
+    path: str
+    files: List[File] = []
+    folders: List["Folder"] = []
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(File)
+    store.register_model(Folder)
+
+    child_folder = Folder(
+        path="path/to/child-folder",
+        files=[
+            File(path="path/to/foo.txt", type=FileType.TEXT),
+            File(path="path/to/foo.jpg", type=FileType.IMAGE),
+        ],
+    )
+
+    await Folder.insert(
+        Folder(
+            path="path/to/parent-folder",
+            files=[
+                File(path="path/to/bar.txt", type=FileType.TEXT),
+                File(path="path/to/bar.jpg", type=FileType.IMAGE),
+            ],
+            folders=[child_folder],
+        )
+    )
+
+    parent_folder_response = await Folder.select(ids=["path/to/parent-folder"])
+    files_response = await File.select(
+        ids=["path/to/foo.txt", "path/to/foo.jpg", "path/to/bar.txt", "path/to/bar.jpg"]
+    )
+
+    await Folder.update(
+        _id="path/to/child-folder",
+        data={"files": [File(path="path/to/foo.txt", type=FileType.EXEC)]},
+    )
+    updated_parent_folder_response = await Folder.select(ids=["path/to/parent-folder"])
+    updated_file_response = await File.select(ids=["path/to/foo.txt"])
+
+    print("parent folder:")
+    pp.pprint(parent_folder_response)
+    print("\nfiles:")
+    pp.pprint(files_response)
+
+    print("\nindirectly updated parent folder:")
+    pp.pprint(updated_parent_folder_response)
+    print("\nindirectly updated files:")
+    pp.pprint(updated_file_response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/models.py b/docs_src/tutorials/asynchronous/models.py
new file mode 100644
index 00000000..6113f3a7
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/models.py
@@ -0,0 +1,46 @@
+import asyncio
+import pprint
+from datetime import date
+from typing import List
+
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(Book)
+
+    await Book.insert(
+        Book(
+            title="Oliver Twist",
+            author="Charles Dickens",
+            published_on=date(year=1215, month=4, day=4),
+            in_stock=False,
+            rating=2,
+            tags=["Classic"],
+        )
+    )
+
+    response = await Book.select(ids=["Oliver Twist"])
+    pp.pprint(response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/nested-models.py b/docs_src/tutorials/asynchronous/nested-models.py
new file mode 100644
index 00000000..c417fee8
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/nested-models.py
@@ -0,0 +1,72 @@
+import asyncio
+import pprint
+from datetime import date
+from typing import List, Tuple
+
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class Author(Model):
+    _primary_key_field: str = "name"
+    name: str
+    active_years: Tuple[int, int]
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: Author
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(Author)
+    store.register_model(Book)
+
+    await Book.insert(
+        Book(
+            title="Oliver Twist",
+            author=Author(name="Charles Dickens", active_years=(1999, 2007)),
+            published_on=date(year=1215, month=4, day=4),
+            in_stock=False,
+            rating=2,
+            tags=["Classic"],
+        )
+    )
+
+    book_response = await Book.select(ids=["Oliver Twist"])
+    author_response = await Author.select(ids=["Charles Dickens"])
+
+    await Author.update(_id="Charles Dickens", data={"active_years": (1227, 1277)})
+    updated_book_response = await Book.select(ids=["Oliver Twist"])
+
+    await Book.update(
+        _id="Oliver Twist",
+        data={"author": Author(name="Charles Dickens", active_years=(1969, 1999))},
+    )
+    updated_author_response = await Author.select(ids=["Charles Dickens"])
+
+    print("book:")
+    pp.pprint(book_response)
+    print("\nauthor:")
+    pp.pprint(author_response)
+
+    print("\nindirectly updated book:")
+    pp.pprint(updated_book_response)
+    print("\nindirectly updated author:")
+    pp.pprint(updated_author_response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/select-records.py b/docs_src/tutorials/asynchronous/select-records.py
new file mode 100644
index 00000000..585b09c4
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/select-records.py
@@ -0,0 +1,60 @@
+import asyncio
+import pprint
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    await Book.insert(
+        [
+            Book(title="Oliver Twist", author="Charles Dickens"),
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+            Book(title="Utah Blaine", author="Louis L'Amour"),
+        ]
+    )
+
+    select_all_response = await Book.select()
+    select_by_id_response = await Book.select(
+        ids=["Oliver Twist", "Pride and Prejudice"]
+    )
+
+    select_some_fields_response = await Book.select(columns=["author"])
+    select_some_fields_for_ids_response = await Book.select(
+        ids=["Oliver Twist", "Pride and Prejudice"], columns=["author"]
+    )
+
+    paginated_select_all_response = await Book.select(skip=0, limit=2)
+    paginated_select_some_fields_response = await Book.select(
+        columns=["author"], skip=2, limit=2
+    )
+
+    print("all:")
+    pp.pprint(select_all_response)
+    print("\nby id:")
+    pp.pprint(select_by_id_response)
+    print("\nsome fields for all:")
+    pp.pprint(select_some_fields_response)
+    print("\nsome fields for given ids:")
+    pp.pprint(select_some_fields_for_ids_response)
+    print("\npaginated; skip: 0, limit: 2:")
+    pp.pprint(paginated_select_all_response)
+    print("\npaginated returning some fields for each; skip: 2, limit: 2:")
+    pp.pprint(paginated_select_some_fields_response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/tuple-of-nested-models.py b/docs_src/tutorials/asynchronous/tuple-of-nested-models.py
new file mode 100644
index 00000000..a9486e6c
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/tuple-of-nested-models.py
@@ -0,0 +1,65 @@
+import asyncio
+import pprint
+from typing import Tuple
+from pydantic_redis.asyncio import RedisConfig, Model, Store
+
+
+class Score(Model):
+    _primary_key_field: str = "id"
+    id: str
+    total: int
+
+
+class ScoreBoard(Model):
+    _primary_key_field: str = "id"
+    id: str
+    scores: Tuple[str, Score]
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(name="test", redis_config=RedisConfig())
+
+    store.register_model(Score)
+    store.register_model(ScoreBoard)
+
+    await ScoreBoard.insert(
+        data=ScoreBoard(
+            id="test",
+            scores=(
+                "mark",
+                Score(id="some id", total=50),
+            ),
+        )
+    )
+    score_board_response = await ScoreBoard.select(ids=["test"])
+    scores_response = await Score.select(ids=["some id"])
+
+    await Score.update(_id="some id", data={"total": 78})
+    updated_score_board_response = await ScoreBoard.select(ids=["test"])
+
+    await ScoreBoard.update(
+        _id="test",
+        data={
+            "scores": (
+                "tom",
+                Score(id="some id", total=60),
+            )
+        },
+    )
+    updated_score_response = await Score.select(ids=["some id"])
+
+    print("score board:")
+    pp.pprint(score_board_response)
+    print("\nscores:")
+    pp.pprint(scores_response)
+
+    print("\nindirectly updated score board:")
+    pp.pprint(updated_score_board_response)
+    print("\nindirectly updated score:")
+    pp.pprint(updated_score_response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/asynchronous/update.py b/docs_src/tutorials/asynchronous/update.py
new file mode 100644
index 00000000..1cc2cc5e
--- /dev/null
+++ b/docs_src/tutorials/asynchronous/update.py
@@ -0,0 +1,52 @@
+import asyncio
+import pprint
+from pydantic_redis.asyncio import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+async def main():
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    await Book.insert(
+        [
+            Book(title="Oliver Twist", author="Charles Dickens"),
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+        ]
+    )
+    await Book.update(_id="Oliver Twist", data={"author": "Charlie Ickens"})
+    await Book.update(
+        _id="Jane Eyre", data={"author": "Daniel McKenzie"}, life_span_seconds=1800
+    )
+    single_update_response = await Book.select()
+
+    await Book.insert(
+        [
+            Book(title="Oliver Twist", author="Chuck Dickens"),
+            Book(title="Jane Eyre", author="Emiliano Bronte"),
+            Book(title="Pride and Prejudice", author="Janey Austen"),
+        ],
+        life_span_seconds=3600,
+    )
+    multi_update_response = await Book.select()
+
+    print("single update:")
+    pp.pprint(single_update_response)
+
+    print("\nmulti update:")
+    pp.pprint(multi_update_response)
+
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
diff --git a/docs_src/tutorials/synchronous/delete.py b/docs_src/tutorials/synchronous/delete.py
new file mode 100644
index 00000000..2087a642
--- /dev/null
+++ b/docs_src/tutorials/synchronous/delete.py
@@ -0,0 +1,36 @@
+import pprint
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    Book.insert(
+        [
+            Book(title="Oliver Twist", author="Charles Dickens"),
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+            Book(title="Utah Blaine", author="Louis L'Amour"),
+        ]
+    )
+    pre_delete_response = Book.select()
+
+    Book.delete(ids=["Oliver Twist", "Pride and Prejudice"])
+    post_delete_response = Book.select()
+
+    print("pre-delete:")
+    pp.pprint(pre_delete_response)
+
+    print("\npost-delete:")
+    pp.pprint(post_delete_response)
diff --git a/docs_src/tutorials/synchronous/insert.py b/docs_src/tutorials/synchronous/insert.py
new file mode 100644
index 00000000..6c87d792
--- /dev/null
+++ b/docs_src/tutorials/synchronous/insert.py
@@ -0,0 +1,39 @@
+import pprint
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    Book.insert(Book(title="Oliver Twist", author="Charles Dickens"))
+    Book.insert(
+        Book(title="Great Expectations", author="Charles Dickens"),
+        life_span_seconds=1800,
+    )
+    Book.insert(
+        [
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+        ]
+    )
+    Book.insert(
+        [
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+        ],
+        life_span_seconds=3600,
+    )
+
+    response = Book.select()
+    pp.pprint(response)
diff --git a/docs_src/tutorials/synchronous/list-of-nested-models.py b/docs_src/tutorials/synchronous/list-of-nested-models.py
new file mode 100644
index 00000000..fa4961a0
--- /dev/null
+++ b/docs_src/tutorials/synchronous/list-of-nested-models.py
@@ -0,0 +1,77 @@
+import pprint
+from enum import Enum
+from typing import List
+
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class FileType(Enum):
+    TEXT = "text"
+    IMAGE = "image"
+    EXEC = "executable"
+
+
+class File(Model):
+    _primary_key_field: str = "path"
+    path: str
+    type: FileType
+
+
+class Folder(Model):
+    _primary_key_field: str = "path"
+    path: str
+    files: List[File] = []
+    folders: List["Folder"] = []
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(File)
+    store.register_model(Folder)
+
+    child_folder = Folder(
+        path="path/to/child-folder",
+        files=[
+            File(path="path/to/foo.txt", type=FileType.TEXT),
+            File(path="path/to/foo.jpg", type=FileType.IMAGE),
+        ],
+    )
+
+    Folder.insert(
+        Folder(
+            path="path/to/parent-folder",
+            files=[
+                File(path="path/to/bar.txt", type=FileType.TEXT),
+                File(path="path/to/bar.jpg", type=FileType.IMAGE),
+            ],
+            folders=[child_folder],
+        )
+    )
+
+    parent_folder_response = Folder.select(ids=["path/to/parent-folder"])
+    files_response = File.select(
+        ids=["path/to/foo.txt", "path/to/foo.jpg", "path/to/bar.txt", "path/to/bar.jpg"]
+    )
+
+    Folder.update(
+        _id="path/to/child-folder",
+        data={"files": [File(path="path/to/foo.txt", type=FileType.EXEC)]},
+    )
+    updated_parent_folder_response = Folder.select(ids=["path/to/parent-folder"])
+    updated_file_response = File.select(ids=["path/to/foo.txt"])
+
+    print("parent folder:")
+    pp.pprint(parent_folder_response)
+    print("\nfiles:")
+    pp.pprint(files_response)
+
+    print("\nindirectly updated parent folder:")
+    pp.pprint(updated_parent_folder_response)
+    print("\nindirectly updated files:")
+    pp.pprint(updated_file_response)
diff --git a/docs_src/tutorials/synchronous/models.py b/docs_src/tutorials/synchronous/models.py
new file mode 100644
index 00000000..63f987d2
--- /dev/null
+++ b/docs_src/tutorials/synchronous/models.py
@@ -0,0 +1,40 @@
+import pprint
+from datetime import date
+from typing import List
+
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(Book)
+
+    Book.insert(
+        Book(
+            title="Oliver Twist",
+            author="Charles Dickens",
+            published_on=date(year=1215, month=4, day=4),
+            in_stock=False,
+            rating=2,
+            tags=["Classic"],
+        )
+    )
+
+    response = Book.select(ids=["Oliver Twist"])
+    pp.pprint(response)
diff --git a/docs_src/tutorials/synchronous/nested-models.py b/docs_src/tutorials/synchronous/nested-models.py
new file mode 100644
index 00000000..2327595c
--- /dev/null
+++ b/docs_src/tutorials/synchronous/nested-models.py
@@ -0,0 +1,66 @@
+import pprint
+from datetime import date
+from typing import List, Tuple
+
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class Author(Model):
+    _primary_key_field: str = "name"
+    name: str
+    active_years: Tuple[int, int]
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: Author
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name",
+        redis_config=RedisConfig(db=5, host="localhost", port=6379),
+        life_span_in_seconds=3600,
+    )
+
+    store.register_model(Author)
+    store.register_model(Book)
+
+    Book.insert(
+        Book(
+            title="Oliver Twist",
+            author=Author(name="Charles Dickens", active_years=(1999, 2007)),
+            published_on=date(year=1215, month=4, day=4),
+            in_stock=False,
+            rating=2,
+            tags=["Classic"],
+        )
+    )
+
+    book_response = Book.select(ids=["Oliver Twist"])
+    author_response = Author.select(ids=["Charles Dickens"])
+
+    Author.update(_id="Charles Dickens", data={"active_years": (1227, 1277)})
+    updated_book_response = Book.select(ids=["Oliver Twist"])
+
+    Book.update(
+        _id="Oliver Twist",
+        data={"author": Author(name="Charles Dickens", active_years=(1969, 1999))},
+    )
+    updated_author_response = Author.select(ids=["Charles Dickens"])
+
+    print("book:")
+    pp.pprint(book_response)
+    print("\nauthor:")
+    pp.pprint(author_response)
+
+    print("\nindirectly updated book:")
+    pp.pprint(updated_book_response)
+    print("\nindirectly updated author:")
+    pp.pprint(updated_author_response)
diff --git a/docs_src/tutorials/synchronous/select-records.py b/docs_src/tutorials/synchronous/select-records.py
new file mode 100644
index 00000000..1f4bc5fc
--- /dev/null
+++ b/docs_src/tutorials/synchronous/select-records.py
@@ -0,0 +1,52 @@
+import pprint
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    Book.insert(
+        [
+            Book(title="Oliver Twist", author="Charles Dickens"),
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+            Book(title="Utah Blaine", author="Louis L'Amour"),
+        ]
+    )
+
+    select_all_response = Book.select()
+    select_by_id_response = Book.select(ids=["Oliver Twist", "Pride and Prejudice"])
+
+    select_some_fields_response = Book.select(columns=["author"])
+    select_some_fields_for_ids_response = Book.select(
+        ids=["Oliver Twist", "Pride and Prejudice"], columns=["author"]
+    )
+
+    paginated_select_all_response = Book.select(skip=0, limit=2)
+    paginated_select_some_fields_response = Book.select(
+        columns=["author"], skip=2, limit=2
+    )
+
+    print("all:")
+    pp.pprint(select_all_response)
+    print("\nby id:")
+    pp.pprint(select_by_id_response)
+    print("\nsome fields for all:")
+    pp.pprint(select_some_fields_response)
+    print("\nsome fields for given ids:")
+    pp.pprint(select_some_fields_for_ids_response)
+    print("\npaginated; skip: 0, limit: 2:")
+    pp.pprint(paginated_select_all_response)
+    print("\npaginated returning some fields for each; skip: 2, limit: 2:")
+    pp.pprint(paginated_select_some_fields_response)
diff --git a/docs_src/tutorials/synchronous/tuple-of-nested-models.py b/docs_src/tutorials/synchronous/tuple-of-nested-models.py
new file mode 100644
index 00000000..89f95313
--- /dev/null
+++ b/docs_src/tutorials/synchronous/tuple-of-nested-models.py
@@ -0,0 +1,59 @@
+import pprint
+from typing import Tuple
+from pydantic_redis import RedisConfig, Model, Store
+
+
+class Score(Model):
+    _primary_key_field: str = "id"
+    id: str
+    total: int
+
+
+class ScoreBoard(Model):
+    _primary_key_field: str = "id"
+    id: str
+    scores: Tuple[str, Score]
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(name="test", redis_config=RedisConfig())
+
+    store.register_model(Score)
+    store.register_model(ScoreBoard)
+
+    ScoreBoard.insert(
+        data=ScoreBoard(
+            id="test",
+            scores=(
+                "mark",
+                Score(id="some id", total=50),
+            ),
+        )
+    )
+    score_board_response = ScoreBoard.select(ids=["test"])
+    scores_response = Score.select(ids=["some id"])
+
+    Score.update(_id="some id", data={"total": 78})
+    updated_score_board_response = ScoreBoard.select(ids=["test"])
+
+    ScoreBoard.update(
+        _id="test",
+        data={
+            "scores": (
+                "tom",
+                Score(id="some id", total=60),
+            )
+        },
+    )
+    updated_score_response = Score.select(ids=["some id"])
+
+    print("score board:")
+    pp.pprint(score_board_response)
+    print("\nscores:")
+    pp.pprint(scores_response)
+
+    print("\nindirectly updated score board:")
+    pp.pprint(updated_score_board_response)
+    print("\nindirectly updated score:")
+    pp.pprint(updated_score_response)
diff --git a/docs_src/tutorials/synchronous/update.py b/docs_src/tutorials/synchronous/update.py
new file mode 100644
index 00000000..ff310e17
--- /dev/null
+++ b/docs_src/tutorials/synchronous/update.py
@@ -0,0 +1,46 @@
+import pprint
+from pydantic_redis import Model, Store, RedisConfig
+
+
+class Book(Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: str
+
+
+if __name__ == "__main__":
+    pp = pprint.PrettyPrinter(indent=4)
+    store = Store(
+        name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400
+    )
+
+    store.register_model(Book)
+
+    Book.insert(
+        [
+            Book(title="Oliver Twist", author="Charles Dickens"),
+            Book(title="Jane Eyre", author="Emily Bronte"),
+            Book(title="Pride and Prejudice", author="Jane Austen"),
+        ]
+    )
+    Book.update(_id="Oliver Twist", data={"author": "Charlie Ickens"})
+    Book.update(
+        _id="Jane Eyre", data={"author": "Daniel McKenzie"}, life_span_seconds=1800
+    )
+    single_update_response = Book.select()
+
+    Book.insert(
+        [
+            Book(title="Oliver Twist", author="Chuck Dickens"),
+            Book(title="Jane Eyre", author="Emiliano Bronte"),
+            Book(title="Pride and Prejudice", author="Janey Austen"),
+        ],
+        life_span_seconds=3600,
+    )
+    multi_update_response = Book.select()
+
+    print("single update:")
+    pp.pprint(single_update_response)
+
+    print("\nmulti update:")
+    pp.pprint(multi_update_response)
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..3f856184
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,71 @@
+site_name: Pydantic-redis
+site_description: Pydantic-redis, simple declarative ORM for redis
+site_url: https://sopherapps.github.io/pydantic-redis/
+
+theme:
+  name: material
+  palette:
+    - media: '(prefers-color-scheme: light)'
+      scheme: default
+      toggle:
+        icon: material/lightbulb
+        name: Switch to light mode
+    - media: '(prefers-color-scheme: dark)'
+      scheme: slate
+      toggle:
+        icon: material/lightbulb-outline
+        name: Switch to dark mode
+  features:
+    - search.suggest
+    - search.highlight
+    - content.tabs.link
+
+plugins:
+  - search
+  - mkdocstrings
+
+repo_name: sopherapps/pydantic-redis
+repo_url: https://github.com/sopherapps/pydantic-redis
+
+nav:
+  - 'Pydantic-redis': index.md
+  - Tutorials:
+      - tutorials/intro.md
+      - Synchronous API:
+          - tutorials/synchronous/models.md
+          - tutorials/synchronous/insert.md
+          - tutorials/synchronous/update.md
+          - tutorials/synchronous/delete.md
+          - tutorials/synchronous/select.md
+          - tutorials/synchronous/nested-models.md
+          - tutorials/synchronous/list-of-nested-models.md
+          - tutorials/synchronous/tuple-of-nested-models.md
+      - Asynchronous API:
+          - tutorials/asynchronous/models.md
+          - tutorials/asynchronous/insert.md
+          - tutorials/asynchronous/update.md
+          - tutorials/asynchronous/delete.md
+          - tutorials/asynchronous/select.md
+          - tutorials/asynchronous/nested-models.md
+          - tutorials/asynchronous/list-of-nested-models.md
+          - tutorials/asynchronous/tuple-of-nested-models.md
+  - 'Explanation':
+      - explanation/why-use-orms.md
+  - reference.md
+  - change-log.md
+
+markdown_extensions:
+  - admonition
+  - pymdownx.highlight:
+      anchor_linenums: true
+  - pymdownx.inlinehilite
+  - pymdownx.snippets
+  - pymdownx.superfences
+  - mdx_include:
+      base_path: docs
+extra_css:
+  - css/termynal.css
+  - css/custom.css
+extra_javascript:
+  - js/termynal.js
+  - js/custom.js
\ No newline at end of file
diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py
index 8873263a..0d09af08 100644
--- a/pydantic_redis/__init__.py
+++ b/pydantic_redis/__init__.py
@@ -1,4 +1,15 @@
-"""Entry point for redisy"""
+"""A simple declarative ORM for redis based on pydantic.
+
+Provides:
+
+1. A subclass-able `Model` class to create Object Relational Mapping to
+redis hashes
+2. A redis `Store` class to mutate and query `Model`'s registered in it
+3. A `RedisConfig` class to pass to the `Store` constructor to connect
+to a redis instance
+4. A synchronous `syncio` and an asynchronous `asyncio` interface to the
+above classes
+"""
 
 from pydantic_redis.syncio import Store, Model, RedisConfig
 import pydantic_redis.asyncio
diff --git a/pydantic_redis/_shared/__init__.py b/pydantic_redis/_shared/__init__.py
new file mode 100644
index 00000000..cfecf191
--- /dev/null
+++ b/pydantic_redis/_shared/__init__.py
@@ -0,0 +1,7 @@
+"""Exposes shared utilities and base classes for pydantic-redis
+
+This includes basic functionality of mutating and querying
+redis via the pydantic-redis ORM regardless of whether this
+is done asynchronously or synchronously.
+This is a private package.
+"""
diff --git a/pydantic_redis/shared/lua_scripts.py b/pydantic_redis/_shared/lua_scripts.py
similarity index 89%
rename from pydantic_redis/shared/lua_scripts.py
rename to pydantic_redis/_shared/lua_scripts.py
index 8221e501..433c8646 100644
--- a/pydantic_redis/shared/lua_scripts.py
+++ b/pydantic_redis/_shared/lua_scripts.py
@@ -1,4 +1,18 @@
-"""Module containing the lua scripts used in select queries"""
+"""Exposes the redis lua scripts to be used in select queries.
+
+Attributes:
+    SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting all records from redis
+    PAGINATED_SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting a slice of all records from redis,
+        given a `limit` maximum number of records to return and a `skip` number of records to skip.
+    SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT: the script for selecting some records from redis, given a bunch of `ids`
+    SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting all records, but returning only a subset of
+        the fields in each record.
+    PAGINATED_SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting a slice of all records from redis,
+        given a `limit` maximum number of records to return and a `skip` number of records to skip, but returning
+        only a subset of the fields in each record.
+    SELECT_SOME_FIELDS_FOR_SOME_IDS_SCRIPT: the script for selecting some records from redis, given a bunch of `ids`,
+        but returning only a subset of the fields in each record.
+"""
 
 SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT = """
 local s_find = string.find
diff --git a/pydantic_redis/_shared/model/__init__.py b/pydantic_redis/_shared/model/__init__.py
new file mode 100644
index 00000000..c6d48444
--- /dev/null
+++ b/pydantic_redis/_shared/model/__init__.py
@@ -0,0 +1,8 @@
+"""Exposes the utilities and the base classes for models.
+
+This includes basic functionality of mutating and querying
+redis via the pydantic-redis ORM regardless of whether this
+is done asynchronously or synchronously.
+"""
+
+from .base import AbstractModel
diff --git a/pydantic_redis/_shared/model/base.py b/pydantic_redis/_shared/model/base.py
new file mode 100644
index 00000000..95f3c3f9
--- /dev/null
+++ b/pydantic_redis/_shared/model/base.py
@@ -0,0 +1,258 @@
+"""Exposes the Base `Model` common to both async and sync APIs
+
+"""
+import typing
+from typing import Dict, Tuple, Any, Type, Union, List, Optional
+
+from pydantic import BaseModel
+
+from pydantic_redis._shared.utils import (
+    typing_get_origin,
+    typing_get_args,
+    from_any_to_valid_redis_type,
+    from_dict_to_key_value_list,
+    from_bytes_to_str,
+    from_str_or_bytes_to_any,
+)
+
+
+from ..store import AbstractStore
+
+
+class AbstractModel(BaseModel):
+    """A base class for all Models, sync and async alike.
+
+    See the child classes for more.
+
+    Attributes:
+        _primary_key_field (str): the field that can uniquely identify each record
+            for the current Model
+        _field_types (Dict[str, Any]): a mapping of the fields and their types for
+            the current model
+        _store (AbstractStore): the Store in which the current model is registered.
+        _nested_model_tuple_fields (Dict[str, Tuple[Any, ...]]): a mapping of
+            fields and their types for fields that have tuples of nested models
+        _nested_model_list_fields (Dict[str, Type["AbstractModel"]]): a mapping of
+            fields and their associated nested models for fields that have
+            lists of nested models
+        _nested_model_fields (Dict[str, Type["AbstractModel"]]): a mapping of
+            fields and their associated nested models for fields that have nested models
+    """
+
+    _primary_key_field: str
+    _field_types: Dict[str, Any] = {}
+    _store: AbstractStore
+    _nested_model_tuple_fields: Dict[str, Tuple[Any, ...]] = {}
+    _nested_model_list_fields: Dict[str, Type["AbstractModel"]] = {}
+    _nested_model_fields: Dict[str, Type["AbstractModel"]] = {}
+
+    class Config:
+        arbitrary_types_allowed = True
+
+    @classmethod
+    def get_store(cls) -> AbstractStore:
+        """Gets the Store in which the current model is registered.
+
+        Returns:
+             the instance of the store for this model
+        """
+        return cls._store
+
+    @classmethod
+    def get_nested_model_tuple_fields(cls):
+        """Gets the mapping for fields that have tuples of nested models.
+
+        Returns:
+             The mapping of field name and field type of a form similar to
+              `Tuple[str, Book, date]`
+        """
+        return cls._nested_model_tuple_fields
+
+    @classmethod
+    def get_nested_model_list_fields(cls):
+        """Gets the mapping for fields that have lists of nested models.
+
+        Returns:
+             The mapping of field name and model class nested in that field.
+        """
+        return cls._nested_model_list_fields
+
+    @classmethod
+    def get_nested_model_fields(cls):
+        """Gets the mapping for fields that have nested models.
+
+        Returns:
+             The mapping of field name and model class nested in that field.
+        """
+        return cls._nested_model_fields
+
+    @classmethod
+    def get_primary_key_field(cls):
+        """Gets the field that can uniquely identify each record of current Model
+
+        Returns:
+            the field that can be used to uniquely identify each record of current Model
+        """
+        return cls._primary_key_field
+
+    @classmethod
+    def get_field_types(cls) -> Dict[str, Any]:
+        """Gets the mapping of field and field_type for current Model.
+
+        Returns:
+            the mapping of field and field_type for current Model
+        """
+        return cls._field_types
+
+    @classmethod
+    def initialize(cls):
+        """Initializes class-wide variables for performance's reasons.
+
+        This is a performance hack that initializes an variables that are common
+        to all instances of the current Model e.g. the field types.
+        """
+        cls._field_types = typing.get_type_hints(cls)
+
+        cls._nested_model_list_fields = {}
+        cls._nested_model_tuple_fields = {}
+        cls._nested_model_fields = {}
+
+        for field, field_type in cls._field_types.items():
+            try:
+                # In case the annotation is Optional, an alias of Union[X, None], extract the X
+                is_generic = hasattr(field_type, "__origin__")
+                if (
+                    is_generic
+                    and typing_get_origin(field_type) == Union
+                    and typing_get_args(field_type)[-1] == None.__class__
+                ):
+                    field_type = typing_get_args(field_type)[0]
+                    is_generic = hasattr(field_type, "__origin__")
+
+                if (
+                    is_generic
+                    and typing_get_origin(field_type) in (List, list)
+                    and issubclass(typing_get_args(field_type)[0], AbstractModel)
+                ):
+                    cls._nested_model_list_fields[field] = typing_get_args(field_type)[
+                        0
+                    ]
+
+                elif (
+                    is_generic
+                    and typing_get_origin(field_type) in (Tuple, tuple)
+                    and any(
+                        [
+                            issubclass(v, AbstractModel)
+                            for v in typing_get_args(field_type)
+                        ]
+                    )
+                ):
+                    cls._nested_model_tuple_fields[field] = typing_get_args(field_type)
+
+                elif issubclass(field_type, AbstractModel):
+                    cls._nested_model_fields[field] = field_type
+
+            except (TypeError, AttributeError):
+                pass
+
+    @classmethod
+    def serialize_partially(cls, data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
+        """Casts complex data types within a given dictionary to valid redis types.
+
+        Args:
+            data: the dictionary containing data with complex data types
+
+        Returns:
+            the transformed dictionary
+        """
+        return {key: from_any_to_valid_redis_type(value) for key, value in data.items()}
+
+    @classmethod
+    def deserialize_partially(
+        cls, data: Union[List[Any], Dict[Any, Any]] = ()
+    ) -> Dict[str, Any]:
+        """Casts str or bytes in a dict or flattened key-value list to expected data types.
+
+        Converts str or bytes to their expected data types
+
+        Args:
+            data: flattened list of key-values or dictionary of data to cast.
+                Keeping it as potentially a dictionary ensures backward compatibility.
+
+        Returns:
+            the dictionary of properly parsed key-values.
+        """
+        if isinstance(data, dict):
+            # for backward compatibility
+            data = from_dict_to_key_value_list(data)
+
+        parsed_dict = {}
+
+        nested_model_list_fields = cls.get_nested_model_list_fields()
+        nested_model_tuple_fields = cls.get_nested_model_tuple_fields()
+        nested_model_fields = cls.get_nested_model_fields()
+
+        for i in range(0, len(data), 2):
+            key = from_bytes_to_str(data[i])
+            field_type = cls._field_types.get(key)
+            value = from_str_or_bytes_to_any(value=data[i + 1], field_type=field_type)
+
+            if key in nested_model_list_fields and value is not None:
+                value = _cast_lists(value, nested_model_list_fields[key])
+
+            elif key in nested_model_tuple_fields and value is not None:
+                value = _cast_tuples(value, nested_model_tuple_fields[key])
+
+            elif key in nested_model_fields and value is not None:
+                value = _cast_to_model(value=value, model=nested_model_fields[key])
+
+            parsed_dict[key] = value
+
+        return parsed_dict
+
+
+def _cast_lists(value: List[Any], _type: Type[AbstractModel]) -> List[AbstractModel]:
+    """Casts a list of flattened key-value lists into a list of _type.
+
+    Args:
+        _type: the type to cast the records to.
+        value: the value to convert
+
+    Returns:
+        a list of records of the given _type
+    """
+    return [_type(**_type.deserialize_partially(item)) for item in value]
+
+
+def _cast_tuples(value: List[Any], _type: Tuple[Any, ...]) -> Tuple[Any, ...]:
+    """Casts a list of flattened key-value lists into a list of tuple of _type,.
+
+    Args:
+        _type: the tuple signature type to cast the records to
+            e.g. Tuple[str, Book, int]
+        value: the value to convert
+
+    Returns:
+        a list of records of tuple signature specified by `_type`
+    """
+    items = []
+    for field_type, value in zip(_type, value):
+        if issubclass(field_type, AbstractModel) and value is not None:
+            value = field_type(**field_type.deserialize_partially(value))
+        items.append(value)
+
+    return tuple(items)
+
+
+def _cast_to_model(value: List[Any], model: Type[AbstractModel]) -> AbstractModel:
+    """Converts a list of flattened key-value lists into a list of models,.
+
+    Args:
+        model: the model class to cast to
+        value: the value to cast
+
+    Returns:
+        a list of model instances of type `model`
+    """
+    return model(**model.deserialize_partially(value))
diff --git a/pydantic_redis/_shared/model/delete_utils.py b/pydantic_redis/_shared/model/delete_utils.py
new file mode 100644
index 00000000..c3905590
--- /dev/null
+++ b/pydantic_redis/_shared/model/delete_utils.py
@@ -0,0 +1,38 @@
+"""Exposes shared utilities for deleting records from redis"""
+from typing import Type, Union, List
+
+from redis.client import Pipeline
+from redis.asyncio.client import Pipeline as AioPipeline
+
+from pydantic_redis._shared.model import AbstractModel
+from pydantic_redis._shared.model.prop_utils import get_redis_key, get_model_index_key
+
+
+def delete_on_pipeline(
+    model: Type[AbstractModel], pipeline: Union[Pipeline, AioPipeline], ids: List[str]
+):
+    """Adds delete operations for the given ids to the redis pipeline.
+
+    Args:
+        model: the Model from which the given records are to be deleted.
+        pipeline: the Redis pipeline on which the delete operations are
+            to be added.
+        ids: the list of primary keys of the records that are to be removed.
+
+    Later when pipeline.execute is called later, the actual deletion occurs
+    """
+    primary_keys = []
+
+    if isinstance(ids, list):
+        primary_keys = ids
+    elif ids is not None:
+        primary_keys = [ids]
+
+    names = [
+        get_redis_key(model=model, primary_key_value=primary_key_value)
+        for primary_key_value in primary_keys
+    ]
+    pipeline.delete(*names)
+    # remove the primary keys from the indexz
+    table_index_key = get_model_index_key(model=model)
+    pipeline.zrem(table_index_key, *names)
diff --git a/pydantic_redis/shared/model/insert_utils.py b/pydantic_redis/_shared/model/insert_utils.py
similarity index 60%
rename from pydantic_redis/shared/model/insert_utils.py
rename to pydantic_redis/_shared/model/insert_utils.py
index 2492d6db..ef45f767 100644
--- a/pydantic_redis/shared/model/insert_utils.py
+++ b/pydantic_redis/_shared/model/insert_utils.py
@@ -1,4 +1,6 @@
-"""Module containing the mixin for insert functionality in model"""
+"""Exposes the utility functions for inserting records into redis.
+
+"""
 from datetime import datetime
 from typing import Union, Optional, Any, Dict, Tuple, List, Type
 
@@ -6,8 +8,8 @@
 from redis.client import Pipeline
 
 from .prop_utils import (
-    get_primary_key,
-    get_table_index_key,
+    get_redis_key,
+    get_model_index_key,
     NESTED_MODEL_PREFIX,
     NESTED_MODEL_LIST_FIELD_PREFIX,
     NESTED_MODEL_TUPLE_FIELD_PREFIX,
@@ -23,23 +25,37 @@ def insert_on_pipeline(
     record: Union[AbstractModel, Dict[str, Any]],
     life_span: Optional[Union[float, int]] = None,
 ) -> Any:
-    """
-    Creates insert commands for the given record on the given pipeline but does not execute
-    thus the data is not yet persisted in redis
-    Returns the key of the created item
+    """Add an insert operation to the redis pipeline.
+
+    Later when the pipeline.execute is called, the actual inserts occur.
+    This reduces the number of network requests to the redis server thus
+    improving performance.
+
+    Args:
+        model: the Model whose records are to be inserted into redis.
+        pipeline: the Redis pipeline on which the insert operation
+            is to be added.
+        _id: the primary key of the record to be inserted in redis.
+            It is None when inserting, and some value when updating.
+        record: the model instance or dictionary to be inserted into redis.
+        life_span: the time-to-live in seconds for the record to be inserted.
+            (default: None)
+
+    Returns:
+        the primary key of the record that is to be inserted.
     """
     key = _id if _id is not None else getattr(record, model.get_primary_key_field())
-    data = _get_serializable_dict(
+    data = _serialize_nested_models(
         model=model, pipeline=pipeline, record=record, life_span=life_span
     )
-    name = get_primary_key(model=model, primary_key_value=key)
+    name = get_redis_key(model=model, primary_key_value=key)
     mapping = model.serialize_partially(data)
     pipeline.hset(name=name, mapping=mapping)
 
     if life_span is not None:
         pipeline.expire(name=name, time=life_span)
     # save the primary key in an index: a sorted set, whose score is current timestamp
-    table_index_key = get_table_index_key(model)
+    table_index_key = get_model_index_key(model)
     timestamp = datetime.utcnow().timestamp()
     pipeline.zadd(table_index_key, {name: timestamp})
     if life_span is not None:
@@ -48,23 +64,36 @@ def insert_on_pipeline(
     return name
 
 
-def _get_serializable_dict(
+def _serialize_nested_models(
     model: Type[AbstractModel],
     pipeline: Union[Pipeline, AioPipeline],
     record: Union[AbstractModel, Dict[str, Any]],
     life_span: Optional[Union[float, int]] = None,
 ) -> Dict[str, Any]:
-    """
-    Returns a dictionary that can be serialized.
+    """Converts nested models into their primary keys.
+
+    In order to make the record serializable, all nested models including those in
+    lists and tuples of nested models are converted to their primary keys,
+    after being their insert operations have been added to the pipeline.
+
     A few cleanups it does include:
       - Upserting any nested records in `record`
-      - Replacing the keys of nested records with their NESTED_MODEL_PREFIX suffixed versions
+      - Replacing the keys of nested records with their NESTED_MODEL_PREFIX prefixed versions
         e.g. `__author` instead of author
-      - Replacing the keys of lists of nested records with their NESTED_MODEL_LIST_FIELD_PREFIX suffixed versions
+      - Replacing the keys of lists of nested records with their NESTED_MODEL_LIST_FIELD_PREFIX prefixed versions
         e.g. `__%&l_author` instead of author
-      - Replacing the keys of tuples of nested records with their NESTED_MODEL_TUPLE_FIELD_PREFIX suffixed versions
+      - Replacing the keys of tuples of nested records with their NESTED_MODEL_TUPLE_FIELD_PREFIX prefixed versions
         e.g. `__%&l_author` instead of author
       - Replacing the values of nested records with their foreign keys
+
+    Args:
+        model: the model the given record belongs to.
+        pipeline: the redis pipeline on which the redis operations are to be done.
+        record: the model or dictionary whose nested models are to be serialized.
+        life_span: the time-to-live in seconds for the given record (default: None).
+
+    Returns:
+        the partially serialized dict that has no nested models
     """
     data = record.items() if isinstance(record, dict) else record
     new_data = {}
@@ -77,11 +106,11 @@ def _get_serializable_dict(
         key, value = k, v
 
         if key in nested_model_list_fields:
-            key, value = _serialize_nested_model_list_field(
+            key, value = _serialize_list(
                 key=key, value=value, pipeline=pipeline, life_span=life_span
             )
         elif key in nested_model_tuple_fields:
-            key, value = _serialize_nested_model_tuple_field(
+            key, value = _serialize_tuple(
                 key=key,
                 value=value,
                 pipeline=pipeline,
@@ -89,7 +118,7 @@ def _get_serializable_dict(
                 tuple_fields=nested_model_tuple_fields,
             )
         elif key in nested_model_fields:
-            key, value = _serialize_nested_model_field(
+            key, value = _serialize_model(
                 key=key, value=value, pipeline=pipeline, life_span=life_span
             )
 
@@ -97,14 +126,22 @@ def _get_serializable_dict(
     return new_data
 
 
-def _serialize_nested_model_tuple_field(
+def _serialize_tuple(
     key: str,
     value: Tuple[AbstractModel],
     pipeline: Union[Pipeline, AioPipeline],
     life_span: Optional[Union[float, int]],
     tuple_fields: Dict[str, Tuple[Any, ...]],
 ) -> Tuple[str, List[Any]]:
-    """Serializes a key-value pair for a field that has a tuple of nested models"""
+    """Replaces models in a tuple with strings.
+
+    It adds insert operations for the records in the tuple onto the pipeline
+    and returns the tuple with the models replaced by their primary keys as value.
+
+    Returns:
+        key: the original `key` prefixed with NESTED_MODEL_TUPLE_FIELD_PREFIX
+        value: tthe tuple with the models replaced by their primary keys
+    """
     try:
         field_types = tuple_fields.get(key, ())
         value = [
@@ -127,13 +164,21 @@ def _serialize_nested_model_tuple_field(
     return key, value
 
 
-def _serialize_nested_model_list_field(
+def _serialize_list(
     key: str,
     value: List[AbstractModel],
     pipeline: Union[Pipeline, AioPipeline],
     life_span: Optional[Union[float, int]],
 ) -> Tuple[str, List[Any]]:
-    """Serializes a key-value pair for a field that has a list of nested models"""
+    """Casts a list of models into a list of strings
+
+    It adds insert operations for the records in the list onto the pipeline
+    and returns a list of their primary keys as value.
+
+    Returns:
+        key: the original `key` prefixed with NESTED_MODEL_LIST_FIELD_PREFIX
+        value: the list of primary keys of the records to be inserted
+    """
     try:
         value = [
             insert_on_pipeline(
@@ -153,13 +198,21 @@ def _serialize_nested_model_list_field(
     return key, value
 
 
-def _serialize_nested_model_field(
+def _serialize_model(
     key: str,
     value: AbstractModel,
     pipeline: Union[Pipeline, AioPipeline],
     life_span: Optional[Union[float, int]],
 ) -> Tuple[str, str]:
-    """Serializes a key-value pair for a field that has a nested model"""
+    """Casts a model into a string
+
+    It adds an insert operation for the given model onto the pipeline
+    and returns its primary key as value.
+
+    Returns:
+        key: the original `key` prefixed with NESTED_MODEL_PREFIX
+        value: the primary key of the model
+    """
     try:
         value = insert_on_pipeline(
             model=value.__class__,
diff --git a/pydantic_redis/_shared/model/prop_utils.py b/pydantic_redis/_shared/model/prop_utils.py
new file mode 100644
index 00000000..9256a247
--- /dev/null
+++ b/pydantic_redis/_shared/model/prop_utils.py
@@ -0,0 +1,69 @@
+"""Exposes utils for getting properties of the Model
+
+Attributes:
+    NESTED_MODEL_PREFIX (str): the prefix for fields with single nested models
+    NESTED_MODEL_LIST_FIELD_PREFIX (str): the prefix for fields with lists of nested models
+    NESTED_MODEL_TUPLE_FIELD_PREFIX (str): the prefix for fields with tuples of nested models
+"""
+
+from typing import Type, Any
+
+from .base import AbstractModel
+
+NESTED_MODEL_PREFIX = "__"
+NESTED_MODEL_LIST_FIELD_PREFIX = "___"
+NESTED_MODEL_TUPLE_FIELD_PREFIX = "____"
+
+
+def get_redis_key(model: Type[AbstractModel], primary_key_value: Any):
+    """Gets the key used internally in redis for the `primary_key_value` of `model`.
+
+    Args:
+        model: the model for which the key is to be generated
+        primary_key_value: the external facing primary key value
+
+    Returns:
+        the primary key internally used for `primary_key_value` of `model`
+    """
+    return f"{get_redis_key_prefix(model)}{primary_key_value}"
+
+
+def get_redis_key_prefix(model: Type[AbstractModel]):
+    """Gets the prefix for keys used internally in redis for records of `model`.
+
+    Args:
+        model: the model for which the redis key prefix is to be generated
+
+    Returns:
+        the prefix of the all the redis keys that are associated with this model
+    """
+    model_name = model.__name__.lower()
+    return f"{model_name}_%&_"
+
+
+def get_redis_keys_regex(model: Type[AbstractModel]):
+    """Gets the regex for all keys of records of `model` used internally in redis.
+
+    Args:
+        model: the model for which the redis key regex is to be generated
+
+    Returns:
+        the regular expression for all keys of records of `model` used internally in redis
+    """
+    return f"{get_redis_key_prefix(model)}*"
+
+
+def get_model_index_key(model: Type[AbstractModel]):
+    """Gets the key for the index set of `model` used internally in redis.
+
+    The index for each given model stores the primary keys for all records
+    that belong to the given model.
+
+    Args:
+        model: the model whose index is wanted.
+
+    Returns:
+        the the key for the index set of `model` used internally in redis.
+    """
+    table_name = model.__name__.lower()
+    return f"{table_name}__index"
diff --git a/pydantic_redis/_shared/model/select_utils.py b/pydantic_redis/_shared/model/select_utils.py
new file mode 100644
index 00000000..a1f5e77f
--- /dev/null
+++ b/pydantic_redis/_shared/model/select_utils.py
@@ -0,0 +1,213 @@
+"""Exposes utilities for selecting records from redis using lua scripts.
+
+"""
+from typing import List, Any, Type, Union, Awaitable, Optional
+
+from pydantic_redis._shared.model.prop_utils import (
+    NESTED_MODEL_PREFIX,
+    NESTED_MODEL_LIST_FIELD_PREFIX,
+    NESTED_MODEL_TUPLE_FIELD_PREFIX,
+    get_redis_keys_regex,
+    get_redis_key_prefix,
+    get_model_index_key,
+)
+
+
+from .base import AbstractModel
+
+
+def get_select_fields(model: Type[AbstractModel], columns: List[str]) -> List[str]:
+    """Gets the fields to be used for selecting HMAP fields in Redis.
+
+    It replaces any fields in `columns` that correspond to nested records with their
+    `__` prefixed versions.
+
+    Args:
+        model: the model for which the fields for selecting are to be derived.
+        columns: the fields that are to be transformed into fields for selecting.
+
+    Returns:
+        the fields for selecting, with nested fields being given appropriate prefixes.
+    """
+    fields = []
+    nested_model_list_fields = model.get_nested_model_list_fields()
+    nested_model_tuple_fields = model.get_nested_model_tuple_fields()
+    nested_model_fields = model.get_nested_model_fields()
+
+    for col in columns:
+
+        if col in nested_model_fields:
+            col = f"{NESTED_MODEL_PREFIX}{col}"
+        elif col in nested_model_list_fields:
+            col = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{col}"
+        elif col in nested_model_tuple_fields:
+            col = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{col}"
+
+        fields.append(col)
+    return fields
+
+
+def select_all_fields_all_ids(
+    model: Type[AbstractModel],
+    skip: int = 0,
+    limit: Optional[int] = None,
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+    """Retrieves all records of the given model in the redis database.
+
+    Args:
+        model: the Model whose records are to be retrieved.
+        skip: the number of records to skip.
+        limit: the maximum number of records to return. If None, limit is infinity.
+
+    Returns:
+        the list of records from redis, each record being a flattened list of key-values.
+        In case we are using async, an Awaitable of that list is returned instead.
+    """
+    if isinstance(limit, int):
+        return _select_all_ids_all_fields_paginated(model=model, limit=limit, skip=skip)
+    else:
+        table_keys_regex = get_redis_keys_regex(model=model)
+        args = [table_keys_regex]
+        store = model.get_store()
+        return store.select_all_fields_for_all_ids_script(args=args)
+
+
+def select_all_fields_some_ids(
+    model: Type[AbstractModel], ids: List[str]
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+    """Retrieves some records from redis.
+
+    Args:
+        model: the Model whose records are to be retrieved.
+        ids: the list of primary keys of the records to be retrieved.
+
+    Returns:
+        the list of records where each record is a flattened key-value list.
+        In case we are using async, an Awaitable of that list is returned instead.
+    """
+    table_prefix = get_redis_key_prefix(model=model)
+    keys = [f"{table_prefix}{key}" for key in ids]
+    store = model.get_store()
+    return store.select_all_fields_for_some_ids_script(keys=keys)
+
+
+def select_some_fields_all_ids(
+    model: Type[AbstractModel],
+    fields: List[str],
+    skip: int = 0,
+    limit: Optional[int] = None,
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+    """Retrieves records of model from redis, each as with a subset of the fields.
+
+    Args:
+        model: the Model whose records are to be retrieved.
+        fields: the subset of fields to return for each record.
+        skip: the number of records to skip.
+        limit: the maximum number of records to return. If None, limit is infinity.
+
+    Returns:
+        the list of records from redis, each record being a flattened list of key-values.
+        In case we are using async, an Awaitable of that list is returned instead.
+    """
+    columns = get_select_fields(model=model, columns=fields)
+
+    if isinstance(limit, int):
+        return _select_some_fields_all_ids_paginated(
+            model=model, columns=columns, limit=limit, skip=skip
+        )
+    else:
+        table_keys_regex = get_redis_keys_regex(model=model)
+        args = [table_keys_regex, *columns]
+        store = model.get_store()
+        return store.select_some_fields_for_all_ids_script(args=args)
+
+
+def select_some_fields_some_ids(
+    model: Type[AbstractModel], fields: List[str], ids: List[str]
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+    """Retrieves some records of current model from redis, each as with a subset of the fields.
+
+    Args:
+        model: the Model whose records are to be retrieved.
+        fields: the subset of fields to return for each record.
+        ids: the list of primary keys of the records to be retrieved.
+
+    Returns:
+        the list of records from redis, each record being a flattened list of key-values.
+        In case we are using async, an Awaitable of that list is returned instead.
+    """
+    table_prefix = get_redis_key_prefix(model=model)
+    keys = [f"{table_prefix}{key}" for key in ids]
+    columns = get_select_fields(model=model, columns=fields)
+    store = model.get_store()
+    return store.select_some_fields_for_some_ids_script(keys=keys, args=columns)
+
+
+def parse_select_response(
+    model: Type[AbstractModel], response: List[List], as_models: bool
+):
+    """Casts a list of flattened key-value lists into a list of models or dicts.
+
+    It replaces any foreign keys with the related model instances,
+    and converts the list of flattened key-value lists into a list of models or dicts.
+    e.g. [["foo", "bar", "head", 9]] => [{"foo": "bar", "head": 9}]
+
+    Returns:
+        If `as_models` is true, list of models else list of dicts
+    """
+    if len(response) == 0:
+        return None
+
+    if as_models:
+        return [
+            model(**model.deserialize_partially(record))
+            for record in response
+            if record != []
+        ]
+
+    return [model.deserialize_partially(record) for record in response if record != []]
+
+
+def _select_all_ids_all_fields_paginated(
+    model: Type[AbstractModel], limit: int, skip: Optional[int]
+):
+    """Retrieves a slice of all records of the given model in the redis database.
+
+    Args:
+        model: the Model whose records are to be retrieved.
+        skip: the number of records to skip.
+        limit: the maximum number of records to return. If None, limit is infinity.
+
+    Returns:
+        the list of records from redis, each record being a flattened list of key-values.
+        In case we are using async, an Awaitable of that list is returned instead.
+    """
+    if skip is None:
+        skip = 0
+    table_index_key = get_model_index_key(model)
+    args = [table_index_key, skip, limit]
+    store = model.get_store()
+    return store.paginated_select_all_fields_for_all_ids_script(args=args)
+
+
+def _select_some_fields_all_ids_paginated(
+    model: Type[AbstractModel], columns: List[str], limit: int, skip: int
+):
+    """Retrieves a slice of all records of model from redis, each as with a subset of the fields.
+
+    Args:
+        model: the Model whose records are to be retrieved.
+        columns: the subset of fields to return for each record.
+        skip: the number of records to skip.
+        limit: the maximum number of records to return. If None, limit is infinity.
+
+    Returns:
+        the list of records from redis, each record being a flattened list of key-values.
+        In case we are using async, an Awaitable of that list is returned instead.
+    """
+    if skip is None:
+        skip = 0
+    table_index_key = get_model_index_key(model)
+    args = [table_index_key, skip, limit, *columns]
+    store = model.get_store()
+    return store.paginated_select_some_fields_for_all_ids_script(args=args)
diff --git a/pydantic_redis/shared/store.py b/pydantic_redis/_shared/store.py
similarity index 74%
rename from pydantic_redis/shared/store.py
rename to pydantic_redis/_shared/store.py
index 65e10c68..08dba776 100644
--- a/pydantic_redis/shared/store.py
+++ b/pydantic_redis/_shared/store.py
@@ -6,7 +6,7 @@
 from pydantic import BaseModel
 from redis.commands.core import Script, AsyncScript
 
-from .config import RedisConfig
+from ..config import RedisConfig
 from .lua_scripts import (
     SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT,
     SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT,
@@ -23,8 +23,9 @@
 
 
 class AbstractStore(BaseModel):
-    """
-    An abstract class of a store
+    """An abstract class of a store.
+
+    Check the child classes for more definition.
     """
 
     name: str
@@ -67,11 +68,24 @@ def __init__(
         self._register_lua_scripts()
 
     def _connect_to_redis(self) -> Union[Redis, AioRedis]:
-        """Connects the store to redis, returning a proper connection"""
+        """Connects the store to redis.
+
+        Connects to the redis database basing on the `redis_config`
+        attribute of this instance.
+
+        Returns:
+            A connection object to a redis database
+        """
         raise NotImplementedError("implement _connect_to_redis first")
 
     def _register_lua_scripts(self):
-        """Registers the lua scripts for this redis instance"""
+        """Registers the lua scripts for this redis instance.
+
+        In order to save on memory and bandwidth, the redis lua scripts
+        need to be called using EVALSHA instead of EVAL. The latter transfers
+        the scripts to the redis server on every invocation while the former
+        saves the script in redis itself and invokes it using a hashed (SHA) value.
+        """
         self.select_all_fields_for_all_ids_script = self.redis_store.register_script(
             SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT
         )
@@ -96,7 +110,15 @@ def _register_lua_scripts(self):
         )
 
     def register_model(self, model_class: Type["AbstractModel"]):
-        """Registers the model to this store"""
+        """Registers the model to this store.
+
+        Each store manages a number of models. In order to associate
+        a model to a redis database, a Store must register it.
+
+        Args:
+            model_class: the class which represents a given schema of
+                a certain type of records to be saved in redis.
+        """
         if not isinstance(model_class.get_primary_key_field(), str):
             raise NotImplementedError(
                 f"{model_class.__name__} should have a _primary_key_field"
@@ -107,5 +129,12 @@ def register_model(self, model_class: Type["AbstractModel"]):
         self.models[model_class.__name__.lower()] = model_class
 
     def model(self, name: str) -> Type["AbstractModel"]:
-        """Gets a model by name: case insensitive"""
+        """Gets a model by name. This is case insensitive.
+
+        Args:
+            name: the case-insensitive name of the model class
+
+        Returns:
+            the class corresponding to the given name
+        """
         return self.models[name.lower()]
diff --git a/pydantic_redis/_shared/utils.py b/pydantic_redis/_shared/utils.py
new file mode 100644
index 00000000..5ecc2130
--- /dev/null
+++ b/pydantic_redis/_shared/utils.py
@@ -0,0 +1,143 @@
+"""Exposes common utilities.
+
+"""
+import typing
+from typing import Any, Tuple, Optional, Union, Dict, Callable, Type, List
+
+import orjson
+
+
+def strip_leading(word: str, substring: str) -> str:
+    """Strips the leading substring if it exists.
+
+    This is contrary to rstrip which removes each character in the substring
+
+    Args:
+        word: the string to strip from
+        substring: the string to be stripped from the word.
+
+    Returns:
+        the stripped word
+    """
+    if word.startswith(substring):
+        return word[len(substring) :]
+    return word
+
+
+def typing_get_args(v: Any) -> Tuple[Any, ...]:
+    """Gets the __args__ of the annotations of a given typing.
+
+    Args:
+        v: the typing object whose __args__ are required.
+
+    Returns:
+        the __args__ of the item passed
+    """
+    try:
+        return typing.get_args(v)
+    except AttributeError:
+        return getattr(v, "__args__", ()) if v is not typing.Generic else typing.Generic
+
+
+def typing_get_origin(v: Any) -> Optional[Any]:
+    """Gets the __origin__ of the annotations of a given typing.
+
+    Args:
+        v: the typing object whose __origin__ are required.
+
+    Returns:
+        the __origin__ of the item passed
+    """
+    try:
+        return typing.get_origin(v)
+    except AttributeError:
+        return getattr(v, "__origin__", None)
+
+
+def from_bytes_to_str(value: Union[str, bytes]) -> str:
+    """Converts bytes to str.
+
+    Args:
+        value: the potentially bytes object to transform.
+
+    Returns:
+        the string value of the argument passed
+    """
+    if isinstance(value, bytes):
+        return str(value, "utf-8")
+    return value
+
+
+def from_str_or_bytes_to_any(value: Any, field_type: Type) -> Any:
+    """Converts str or bytes to arbitrary data.
+
+    Converts the the `value` from a string or bytes to the `field_type`.
+
+    Args:
+        value: the string or bytes to be transformed to the `field_type`
+        field_type: the type to which value is to be converted
+
+    Returns:
+        the `field_type` version of the `value`.
+    """
+    if isinstance(value, (bytes, bytearray, memoryview)):
+        return orjson.loads(value)
+    elif isinstance(value, str) and field_type != str:
+        return orjson.loads(value)
+    return value
+
+
+def from_any_to_valid_redis_type(value: Any) -> Union[str, bytes, List[Any]]:
+    """Converts arbitrary data into valid redis types
+
+    Converts the the `value` from any type to a type that
+    are acceptable by redis.
+    By default, complex data is transformed to bytes.
+
+    Args:
+        value: the value to be transformed to a valid redis data type
+
+    Returns:
+        the transformed version of the `value`.
+    """
+    if isinstance(value, str):
+        return value
+    elif isinstance(value, set):
+        return list(value)
+    return orjson.dumps(value, default=default_json_dump)
+
+
+def default_json_dump(obj: Any):
+    """Serializes objects orjson cannot serialize.
+
+    Args:
+        obj: the object to serialize
+
+    Returns:
+        the bytes or string value of the object
+    """
+    if hasattr(obj, "json") and isinstance(obj.json, Callable):
+        return obj.json()
+    return obj
+
+
+def from_dict_to_key_value_list(data: Dict[str, Any]) -> List[Any]:
+    """Converts dict to flattened list of key, values.
+
+    {"foo": "bar", "hen": "rooster"} becomes ["foo", "bar", "hen", "rooster"]
+    When redis lua scripts are used, the value returned is a flattened list,
+    similar to that shown above.
+
+    Args:
+        data: the dictionary to flatten
+
+    Returns:
+        the flattened list version of `data`
+    """
+    parsed_list = []
+
+    for k, v in data.items():
+        parsed_list.append(k)
+        parsed_list.append(v)
+
+    return parsed_list
diff --git a/pydantic_redis/asyncio/__init__.py b/pydantic_redis/asyncio/__init__.py
index 168f33ff..1da65b74 100644
--- a/pydantic_redis/asyncio/__init__.py
+++ b/pydantic_redis/asyncio/__init__.py
@@ -1,7 +1,34 @@
-"""Package containing the async version of pydantic_redis"""
+"""Asynchronous API for pydantic-redis ORM.
+
+Typical usage example:
+
+```python
+import asyncio
+from pydantic_redis.asyncio import Store, Model, RedisConfig
+
+class Book(Model):
+    _primary_key_field = 'title'
+    title: str
+
+async def main():
+    store = Store(name="sample", redis_config=RedisConfig())
+    store.register_model(Book)
+
+    await Book.insert(Book(title="Oliver Twist", author="Charles Dickens"))
+    await Book.update(
+        _id="Oliver Twist", data={"author": "Jane Austen"}, life_span_seconds=3600
+    )
+    results = await Book.select()
+    await Book.delete(ids=["Oliver Twist", "Great Expectations"])
+
+if __name__ == "__main__":
+    loop = asyncio.get_event_loop()
+    loop.run_until_complete(main())
+```
+"""
 
 from .model import Model
 from .store import Store
-from ..shared.config import RedisConfig
+from ..config import RedisConfig
 
 __all__ = [Model, Store, RedisConfig]
diff --git a/pydantic_redis/asyncio/model.py b/pydantic_redis/asyncio/model.py
index ea04ddea..5f31a19f 100644
--- a/pydantic_redis/asyncio/model.py
+++ b/pydantic_redis/asyncio/model.py
@@ -1,12 +1,13 @@
-"""Module containing the model classes"""
-from typing import Optional, List, Any, Union, Dict, Tuple, Type
+"""Exposes the Base `Model` class for creating custom asynchronous models.
 
-import redis.asyncio
+This module contains the `Model` class which should be inherited when
+creating model's for use in the asynchronous API of pydantic-redis.
+"""
+from typing import Optional, List, Any, Union, Dict
 
-from pydantic_redis.shared.model import AbstractModel
-from pydantic_redis.shared.model.insert_utils import insert_on_pipeline
-from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key
-from pydantic_redis.shared.model.select_utils import (
+from .._shared.model import AbstractModel
+from .._shared.model.insert_utils import insert_on_pipeline
+from .._shared.model.select_utils import (
     select_all_fields_all_ids,
     select_all_fields_some_ids,
     select_some_fields_all_ids,
@@ -15,12 +16,15 @@
 )
 
 from .store import Store
-from ..shared.model.delete_utils import delete_on_pipeline
+from .._shared.model.delete_utils import delete_on_pipeline
 
 
 class Model(AbstractModel):
-    """
-    The section in the store that saves rows of the same kind
+    """The Base class for all Asynchronous models.
+
+    Inherit this class when creating a new model.
+    The new model should have `_primary_key_field` defined.
+    Any interaction with redis is done through `Model`'s.
     """
 
     _store: Store
@@ -31,8 +35,18 @@ async def insert(
         data: Union[List[AbstractModel], AbstractModel],
         life_span_seconds: Optional[float] = None,
     ):
-        """
-        Inserts a given row or sets of rows into the table
+        """Inserts a given record or list of records into the redis.
+
+        Can add a single record or multiple records into redis.
+        The records must be instances of this class. i.e. a `Book`
+        model can only insert `Book` instances.
+
+        Args:
+            data: a model instance or list of model instances to put
+                into the redis store
+            life_span_seconds: the time-to-live in seconds of the records
+                to be inserted. If not specified, it defaults to the `Store`'s
+                life_span_seconds.
         """
         store = cls.get_store()
         life_span = (
@@ -64,8 +78,17 @@ async def insert(
     async def update(
         cls, _id: Any, data: Dict[str, Any], life_span_seconds: Optional[float] = None
     ):
-        """
-        Updates a given row or sets of rows in the table
+        """Updates the record whose primary key is `_id`.
+
+        Updates the record of this Model in redis whose primary key is equal to the `_id` provided.
+        The record is partially updated from the `data`.
+        If `life_span_seconds` is provided, it will also update the time-to-live of
+        the record.
+
+        Args:
+            _id: the primary key of record to be updated.
+            data: the new changes
+            life_span_seconds: the new time-to-live for the record
         """
         store = cls.get_store()
         life_span = (
@@ -87,8 +110,13 @@ async def update(
 
     @classmethod
     async def delete(cls, ids: Union[Any, List[Any]]):
-        """
-        deletes a given row or sets of rows in the table
+        """Removes a list of this Model's records from redis
+
+        Removes all the records for the current Model whose primary keys
+        have been included in the `ids` passed.
+
+        Args:
+            ids: list of primary keys of the records to remove
         """
         store = cls.get_store()
 
@@ -104,17 +132,31 @@ async def select(
         skip: int = 0,
         limit: Optional[int] = None,
         **kwargs,
-    ):
-        """
-        Selects given rows or sets of rows in the table
+    ) -> Union["Model", Dict[str, Any]]:
+        """etrieves records of this Model from redis.
+
+        Retrieves the records for this Model from redis.
+
+        Args:
+            columns: the fields to return for each record
+            ids: the primary keys of the records to returns
+            skip: the number of records to skip. (default: 0)
+            limit: the maximum number of records to return
+
+        Returns:
+            By default, it returns all records that belong to current Model.
+
+            If `ids` are specified, it returns only records whose primary keys
+            have been listed in `ids`.
 
+            If `skip` and `limit` are specified WITHOUT `ids`, a slice of
+            all records are returned.
 
-        However, if `limit` is set, the number of items
-        returned will be less or equal to `limit`.
-        `skip` defaults to 0. It is the number of items to skip.
-        `skip` is only relevant when limit is specified.
+            If `limit` and `ids` are specified, `limit` is ignored.
 
-        `skip` and `limit` are irrelevant when `ids` are provided.
+            If `columns` are specified, a list of dictionaries containing only
+            the fields specified in `columns` is returned. Otherwise, instances
+            of the current Model are returned.
         """
         if columns is None and ids is None:
             response = await select_all_fields_all_ids(
diff --git a/pydantic_redis/asyncio/store.py b/pydantic_redis/asyncio/store.py
index 4c24ecae..599bbcbd 100644
--- a/pydantic_redis/asyncio/store.py
+++ b/pydantic_redis/asyncio/store.py
@@ -1,23 +1,46 @@
-"""Module containing the store class for async io"""
+"""Exposes the `Store` for managing a collection of asynchronous Model's.
+
+Stores represent a collection of different kinds of records saved in
+a redis database. They only expose records whose `Model`'s have been
+registered in them. Thus redis server can have multiple stores each
+have a different image of the actual data in redis.
+
+A model must be registered with a store before it can interact with
+a redis database.
+"""
 from typing import Dict, Type, TYPE_CHECKING
 
 from redis import asyncio as redis
 
-from ..shared.store import AbstractStore
+from .._shared.store import AbstractStore
 
 if TYPE_CHECKING:
     from .model import Model
 
 
 class Store(AbstractStore):
-    """
-    A store that allows a declarative way of querying for data in redis
+    """Manages a collection of Model's, connecting them to a redis database
+
+    A Model can only interact with a redis database when it is registered
+    with a `Store` that is connected to that database.
+
+    Attributes:
+        models (Dict[str, Type[pydantic_redis.syncio.Model]]): a mapping of registered `Model`'s, with the keys being the
+            Model name
+        name (str): the name of this Store
+        redis_config (pydantic_redis.syncio.RedisConfig): the configuration for connecting to a redis database
+        redis_store (Optional[redis.Redis]): an Redis instance associated with this store (default: None)
+        life_span_in_seconds (Optional[int]): the default time-to-live for the records inserted in this store
+            (default: None)
     """
 
     models: Dict[str, Type["Model"]] = {}
 
     def _connect_to_redis(self) -> redis.Redis:
-        """Connects the store to redis, returning a proper connection"""
+        """Connects the store to redis.
+
+        See the base class.
+        """
         return redis.from_url(
             self.redis_config.redis_url,
             encoding=self.redis_config.encoding,
diff --git a/pydantic_redis/config.py b/pydantic_redis/config.py
new file mode 100644
index 00000000..b9c88676
--- /dev/null
+++ b/pydantic_redis/config.py
@@ -0,0 +1,43 @@
+"""Exposes the configuration for connecting to a redis database.
+"""
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class RedisConfig(BaseModel):
+    """Configuration for connecting to redis database.
+
+    Inorder to connect to a redis database, there are a number of
+    configurations that are needed including the server's host address
+    and port. `RedisConfig` computes a redis-url similar to
+    `redis://:password@host:self.port/db`
+
+    Attributes:
+        host (str): the host address where the redis server is found (default: 'localhost').
+        port (int): the port on which the redis server is running (default: 6379).
+        db (int): the redis database identifier (default: 0).
+        password (Optional[int]): the password for connecting to the
+            redis server (default: None).
+        ssl (bool): whether the connection to the redis server is to be via TLS (default: False)
+        encoding: (Optional[str]): the string encoding used with the redis database
+            (default: utf-8)
+    """
+
+    host: str = "localhost"
+    port: int = 6379
+    db: int = 0
+    password: Optional[str] = None
+    ssl: bool = False
+    encoding: Optional[str] = "utf-8"
+
+    @property
+    def redis_url(self) -> str:
+        """a redis URL of form `redis://:password@host:port/db`. (`rediss://..` if TLS)."""
+        proto = "rediss" if self.ssl else "redis"
+        if self.password is None:
+            return f"{proto}://{self.host}:{self.port}/{self.db}"
+        return f"{proto}://:{self.password}@{self.host}:{self.port}/{self.db}"
+
+    class Config:
+        orm_mode = True
diff --git a/pydantic_redis/shared/__init__.py b/pydantic_redis/shared/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/pydantic_redis/shared/config.py b/pydantic_redis/shared/config.py
deleted file mode 100644
index 73081cb0..00000000
--- a/pydantic_redis/shared/config.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Module containing the main config classes"""
-from typing import Optional
-
-from pydantic import BaseModel
-
-
-class RedisConfig(BaseModel):
-    host: str = "localhost"
-    port: int = 6379
-    db: int = 0
-    password: Optional[str] = None
-    ssl: bool = False
-    encoding: Optional[str] = "utf-8"
-
-    @property
-    def redis_url(self) -> str:
-        """Returns a redis url to connect to"""
-        proto = "rediss" if self.ssl else "redis"
-        if self.password is None:
-            return f"{proto}://{self.host}:{self.port}/{self.db}"
-        return f"{proto}://:{self.password}@{self.host}:{self.port}/{self.db}"
-
-    class Config:
-        orm_mode = True
diff --git a/pydantic_redis/shared/model/__init__.py b/pydantic_redis/shared/model/__init__.py
deleted file mode 100644
index 4b43caa9..00000000
--- a/pydantic_redis/shared/model/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from .base import AbstractModel
diff --git a/pydantic_redis/shared/model/base.py b/pydantic_redis/shared/model/base.py
deleted file mode 100644
index 4b68698c..00000000
--- a/pydantic_redis/shared/model/base.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""Module containing the base model"""
-import typing
-from typing import Dict, Tuple, Any, Type, Union, List, Optional
-
-from pydantic import BaseModel
-
-from pydantic_redis.shared.utils import (
-    typing_get_origin,
-    typing_get_args,
-    from_any_to_str_or_bytes,
-    from_dict_to_key_value_list,
-    from_bytes_to_str,
-    from_str_or_bytes_to_any,
-)
-
-
-from ..store import AbstractStore
-
-
-class AbstractModel(BaseModel):
-    """
-    An abstract class to help with typings for Model class
-    """
-
-    _primary_key_field: str
-    _field_types: Dict[str, Any] = {}
-    _store: AbstractStore
-    _nested_model_tuple_fields: Dict[str, Tuple[Any, ...]] = {}
-    _nested_model_list_fields: Dict[str, Type["AbstractModel"]] = {}
-    _nested_model_fields: Dict[str, Type["AbstractModel"]] = {}
-
-    class Config:
-        arbitrary_types_allowed = True
-
-    @classmethod
-    def get_store(cls) -> AbstractStore:
-        """Returns the instance of the store for this model"""
-        return cls._store
-
-    @classmethod
-    def get_nested_model_tuple_fields(cls):
-        """Returns the fields that have tuples of nested models"""
-        return cls._nested_model_tuple_fields
-
-    @classmethod
-    def get_nested_model_list_fields(cls):
-        """Returns the fields that have list of nested models"""
-        return cls._nested_model_list_fields
-
-    @classmethod
-    def get_nested_model_fields(cls):
-        """Returns the fields that have nested models"""
-        return cls._nested_model_fields
-
-    @classmethod
-    def get_primary_key_field(cls):
-        """Gets the protected _primary_key_field"""
-        return cls._primary_key_field
-
-    @classmethod
-    def get_field_types(cls) -> Dict[str, Any]:
-        """Returns the fields types of this model"""
-        return cls._field_types
-
-    @classmethod
-    def initialize(cls):
-        """Initializes class-wide variables for performance's reasons e.g. it caches the nested model fields"""
-        cls._field_types = typing.get_type_hints(cls)
-
-        cls._nested_model_list_fields = {}
-        cls._nested_model_tuple_fields = {}
-        cls._nested_model_fields = {}
-
-        for field, field_type in cls._field_types.items():
-            try:
-                # In case the annotation is Optional, an alias of Union[X, None], extract the X
-                is_generic = hasattr(field_type, "__origin__")
-                if (
-                    is_generic
-                    and typing_get_origin(field_type) == Union
-                    and typing_get_args(field_type)[-1] == None.__class__
-                ):
-                    field_type = typing_get_args(field_type)[0]
-                    is_generic = hasattr(field_type, "__origin__")
-
-                if (
-                    is_generic
-                    and typing_get_origin(field_type) in (List, list)
-                    and issubclass(typing_get_args(field_type)[0], AbstractModel)
-                ):
-                    cls._nested_model_list_fields[field] = typing_get_args(field_type)[
-                        0
-                    ]
-
-                elif (
-                    is_generic
-                    and typing_get_origin(field_type) in (Tuple, tuple)
-                    and any(
-                        [
-                            issubclass(v, AbstractModel)
-                            for v in typing_get_args(field_type)
-                        ]
-                    )
-                ):
-                    cls._nested_model_tuple_fields[field] = typing_get_args(field_type)
-
-                elif issubclass(field_type, AbstractModel):
-                    cls._nested_model_fields[field] = field_type
-
-            except (TypeError, AttributeError):
-                pass
-
-    @classmethod
-    def serialize_partially(cls, data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
-        """Converts non primitive data types into str"""
-        return {key: from_any_to_str_or_bytes(value) for key, value in data.items()}
-
-    @classmethod
-    def deserialize_partially(
-        cls, data: Union[List[Any], Dict[Any, Any]] = ()
-    ) -> Dict[str, Any]:
-        """
-        Converts str or bytes to their expected data types, given a list of properties got from the
-        list of lists got after calling EVAL on redis.
-
-        EVAL returns a List of Lists of key, values where the value for a given key is in the position
-        just after the key e.g. [["foo", "bar", "head", 9]] => [{"foo": "bar", "head": 9}]
-
-        Note: For backward compatibility, data can also be a dict.
-        """
-        if isinstance(data, dict):
-            # for backward compatibility
-            data = from_dict_to_key_value_list(data)
-
-        parsed_dict = {}
-
-        nested_model_list_fields = cls.get_nested_model_list_fields()
-        nested_model_tuple_fields = cls.get_nested_model_tuple_fields()
-        nested_model_fields = cls.get_nested_model_fields()
-
-        for i in range(0, len(data), 2):
-            key = from_bytes_to_str(data[i])
-            field_type = cls._field_types.get(key)
-            value = from_str_or_bytes_to_any(value=data[i + 1], field_type=field_type)
-
-            if key in nested_model_list_fields and value is not None:
-                value = deserialize_nested_model_list(
-                    field_type=nested_model_list_fields[key], value=value
-                )
-
-            elif key in nested_model_tuple_fields and value is not None:
-                value = deserialize_nested_model_tuple(
-                    field_types=nested_model_tuple_fields[key], value=value
-                )
-
-            elif key in nested_model_fields and value is not None:
-                value = deserialize_nested_model(
-                    field_type=nested_model_fields[key], value=value
-                )
-
-            parsed_dict[key] = value
-
-        return parsed_dict
-
-
-def deserialize_nested_model_list(
-    field_type: Type[AbstractModel], value: List[Any]
-) -> List[AbstractModel]:
-    """Deserializes a list of key values for the given field returning a list of nested models"""
-    return [field_type(**field_type.deserialize_partially(item)) for item in value]
-
-
-def deserialize_nested_model_tuple(
-    field_types: Tuple[Any, ...], value: List[Any]
-) -> Tuple[Any, ...]:
-    """Deserializes a list of key values for the given field returning a tuple of nested models among others"""
-    items = []
-    for field_type, value in zip(field_types, value):
-        if issubclass(field_type, AbstractModel) and value is not None:
-            value = field_type(**field_type.deserialize_partially(value))
-        items.append(value)
-
-    return tuple(items)
-
-
-def deserialize_nested_model(
-    field_type: Type[AbstractModel], value: List[Any]
-) -> AbstractModel:
-    """Deserializes a list of key values for the given field returning the nested model"""
-    return field_type(**field_type.deserialize_partially(value))
diff --git a/pydantic_redis/shared/model/delete_utils.py b/pydantic_redis/shared/model/delete_utils.py
deleted file mode 100644
index 19709111..00000000
--- a/pydantic_redis/shared/model/delete_utils.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Module containing common functionality for deleting records"""
-from typing import Type, Union, List
-
-from redis.client import Pipeline
-from redis.asyncio.client import Pipeline as AioPipeline
-
-from pydantic_redis.shared.model import AbstractModel
-from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key
-
-
-def delete_on_pipeline(
-    model: Type[AbstractModel], pipeline: Union[Pipeline, AioPipeline], ids: List[str]
-):
-    """
-    Pipelines the deletion of the given ids, so that when pipeline.execute
-    is called later, deletion occurs
-    """
-    primary_keys = []
-
-    if isinstance(ids, list):
-        primary_keys = ids
-    elif ids is not None:
-        primary_keys = [ids]
-
-    names = [
-        get_primary_key(model=model, primary_key_value=primary_key_value)
-        for primary_key_value in primary_keys
-    ]
-    pipeline.delete(*names)
-    # remove the primary keys from the indexz
-    table_index_key = get_table_index_key(model=model)
-    pipeline.zrem(table_index_key, *names)
diff --git a/pydantic_redis/shared/model/prop_utils.py b/pydantic_redis/shared/model/prop_utils.py
deleted file mode 100644
index 335b3dda..00000000
--- a/pydantic_redis/shared/model/prop_utils.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Module containing utils for getting properties of the Model"""
-
-from typing import Type, Any
-
-from .base import AbstractModel
-
-NESTED_MODEL_PREFIX = "__"
-NESTED_MODEL_LIST_FIELD_PREFIX = "___"
-NESTED_MODEL_TUPLE_FIELD_PREFIX = "____"
-
-
-def get_primary_key(model: Type[AbstractModel], primary_key_value: Any):
-    """
-    Returns the primary key value concatenated to the table name for uniqueness
-    """
-    return f"{get_table_prefix(model)}{primary_key_value}"
-
-
-def get_table_prefix(model: Type[AbstractModel]):
-    """
-    Returns the prefix of the all the redis keys that are associated with this table
-    """
-    table_name = model.__name__.lower()
-    return f"{table_name}_%&_"
-
-
-def get_table_keys_regex(model: Type[AbstractModel]):
-    """
-    Returns the table name regex to get all keys that belong to this table
-    """
-    return f"{get_table_prefix(model)}*"
-
-
-def get_table_index_key(model: Type[AbstractModel]):
-    """Returns the key for the set in which the primary keys of the given table have been saved"""
-    table_name = model.__name__.lower()
-    return f"{table_name}__index"
diff --git a/pydantic_redis/shared/model/select_utils.py b/pydantic_redis/shared/model/select_utils.py
deleted file mode 100644
index e0b21416..00000000
--- a/pydantic_redis/shared/model/select_utils.py
+++ /dev/null
@@ -1,155 +0,0 @@
-"""Module containing the mixin functionality for selecting"""
-from typing import List, Any, Type, Union, Awaitable, Optional
-
-from pydantic_redis.shared.model.prop_utils import (
-    NESTED_MODEL_PREFIX,
-    NESTED_MODEL_LIST_FIELD_PREFIX,
-    NESTED_MODEL_TUPLE_FIELD_PREFIX,
-    get_table_keys_regex,
-    get_table_prefix,
-    get_table_index_key,
-)
-
-
-from .base import AbstractModel
-
-
-def get_select_fields(model: Type[AbstractModel], columns: List[str]) -> List[str]:
-    """
-    Gets the fields to be used for selecting HMAP fields in Redis
-    It replaces any fields in `columns` that correspond to nested records with their
-    `__` suffixed versions
-    """
-    fields = []
-    nested_model_list_fields = model.get_nested_model_list_fields()
-    nested_model_tuple_fields = model.get_nested_model_tuple_fields()
-    nested_model_fields = model.get_nested_model_fields()
-
-    for col in columns:
-
-        if col in nested_model_fields:
-            col = f"{NESTED_MODEL_PREFIX}{col}"
-        elif col in nested_model_list_fields:
-            col = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{col}"
-        elif col in nested_model_tuple_fields:
-            col = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{col}"
-
-        fields.append(col)
-    return fields
-
-
-def select_all_fields_all_ids(
-    model: Type[AbstractModel],
-    skip: int = 0,
-    limit: Optional[int] = None,
-) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
-    """
-    Selects all items in the database, returning all their fields
-
-    However, if `limit` is set, the number of items
-    returned will be less or equal to `limit`.
-    `skip` defaults to 0. It is the number of items to skip.
-    `skip` is only relevant when limit is specified.
-    """
-    if isinstance(limit, int):
-        return _select_all_ids_all_fields_paginated(model=model, limit=limit, skip=skip)
-    else:
-        table_keys_regex = get_table_keys_regex(model=model)
-        args = [table_keys_regex]
-        store = model.get_store()
-        return store.select_all_fields_for_all_ids_script(args=args)
-
-
-def select_all_fields_some_ids(
-    model: Type[AbstractModel], ids: List[str]
-) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
-    """Selects some items in the database, returning all their fields"""
-    table_prefix = get_table_prefix(model=model)
-    keys = [f"{table_prefix}{key}" for key in ids]
-    store = model.get_store()
-    return store.select_all_fields_for_some_ids_script(keys=keys)
-
-
-def select_some_fields_all_ids(
-    model: Type[AbstractModel],
-    fields: List[str],
-    skip: int = 0,
-    limit: Optional[int] = None,
-) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
-    """
-    Selects all items in the database, returning only the specified fields.
-
-    However, if `limit` is set, the number of items
-    returned will be less or equal to `limit`.
-    `skip` defaults to 0. It is the number of items to skip.
-    `skip` is only relevant when limit is specified.
-    """
-    columns = get_select_fields(model=model, columns=fields)
-
-    if isinstance(limit, int):
-        return _select_some_fields_all_ids_paginated(
-            model=model, columns=columns, limit=limit, skip=skip
-        )
-    else:
-        table_keys_regex = get_table_keys_regex(model=model)
-        args = [table_keys_regex, *columns]
-        store = model.get_store()
-        return store.select_some_fields_for_all_ids_script(args=args)
-
-
-def select_some_fields_some_ids(
-    model: Type[AbstractModel], fields: List[str], ids: List[str]
-) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
-    """Selects some of items in the database, returning only the specified fields"""
-    table_prefix = get_table_prefix(model=model)
-    keys = [f"{table_prefix}{key}" for key in ids]
-    columns = get_select_fields(model=model, columns=fields)
-    store = model.get_store()
-    return store.select_some_fields_for_some_ids_script(keys=keys, args=columns)
-
-
-def parse_select_response(
-    model: Type[AbstractModel], response: List[List], as_models: bool
-):
-    """
-    Converts a list of lists of key-values into a list of models if `as_models` is true or leaves them as dicts
-    with foreign keys replaced by model instances. The list is got from calling EVAL on Redis .
-
-    EVAL returns a List of Lists of key, values where the value for a given key is in the position
-    just after the key e.g. [["foo", "bar", "head", 9]] => [{"foo": "bar", "head": 9}]
-    """
-    if len(response) == 0:
-        return None
-
-    if as_models:
-        return [
-            model(**model.deserialize_partially(record))
-            for record in response
-            if record != []
-        ]
-
-    return [model.deserialize_partially(record) for record in response if record != []]
-
-
-def _select_all_ids_all_fields_paginated(
-    model: Type[AbstractModel], limit: int, skip: Optional[int]
-):
-    """Selects all fields for at most `limit` number of items after skipping `skip` items"""
-    if skip is None:
-        skip = 0
-    table_index_key = get_table_index_key(model)
-    args = [table_index_key, skip, limit]
-    store = model.get_store()
-    return store.paginated_select_all_fields_for_all_ids_script(args=args)
-
-
-def _select_some_fields_all_ids_paginated(
-    model: Type[AbstractModel], columns: List[str], limit: int, skip: int
-):
-    """Selects some fields for at most `limit` number of items after skipping `skip` items"""
-    if skip is None:
-        skip = 0
-    table_index_key = get_table_index_key(model)
-    args = [table_index_key, skip, limit, *columns]
-    store = model.get_store()
-    return store.paginated_select_some_fields_for_all_ids_script(args=args)
diff --git a/pydantic_redis/shared/utils.py b/pydantic_redis/shared/utils.py
deleted file mode 100644
index a76de887..00000000
--- a/pydantic_redis/shared/utils.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""Module containing common utilities"""
-import typing
-from typing import Any, Tuple, Optional, Union, Dict, Callable, Type, List
-
-import orjson
-
-
-def strip_leading(word: str, substring: str) -> str:
-    """
-    Strips the leading substring if it exists.
-    This is contrary to rstrip which can looks at removes each character in the substring
-    """
-    if word.startswith(substring):
-        return word[len(substring) :]
-    return word
-
-
-def typing_get_args(v: Any) -> Tuple[Any, ...]:
-    """Gets the __args__ of the annotations of a given typing"""
-    try:
-        return typing.get_args(v)
-    except AttributeError:
-        return getattr(v, "__args__", ()) if v is not typing.Generic else typing.Generic
-
-
-def typing_get_origin(v: Any) -> Optional[Any]:
-    """Gets the __origin__ of the annotations of a given typing"""
-    try:
-        return typing.get_origin(v)
-    except AttributeError:
-        return getattr(v, "__origin__", None)
-
-
-def from_bytes_to_str(value: Union[str, bytes]) -> str:
-    """Converts bytes to str"""
-    if isinstance(value, bytes):
-        return str(value, "utf-8")
-    return value
-
-
-def from_str_or_bytes_to_any(value: Any, field_type: Type) -> Any:
-    """Converts str or bytes to arbitrary data"""
-    if isinstance(value, (bytes, bytearray, memoryview)):
-        return orjson.loads(value)
-    elif isinstance(value, str) and field_type != str:
-        return orjson.loads(value)
-    return value
-
-
-def from_any_to_str_or_bytes(value: Any) -> Union[str, bytes]:
-    """Converts arbitrary data into str or bytes"""
-    if isinstance(value, str):
-        return value
-    return orjson.dumps(value, default=default_json_dump)
-
-
-def default_json_dump(obj):
-    """Default JSON dump for orjson"""
-    if hasattr(obj, "json") and isinstance(obj.json, Callable):
-        return obj.json()
-    elif isinstance(obj, set):
-        # Set does not exist in JSON.
-        # It's fine to use list instead, it becomes a Set when deserializing.
-        return list(obj)
-
-
-def from_dict_to_key_value_list(data: Dict[str, Any]) -> List[Any]:
-    """Converts dict to flattened list of key, values where the value after the key"""
-    parsed_list = []
-
-    for k, v in data.items():
-        parsed_list.append(k)
-        parsed_list.append(v)
-
-    return parsed_list
diff --git a/pydantic_redis/syncio/__init__.py b/pydantic_redis/syncio/__init__.py
index 2af01c3f..09ab5db6 100644
--- a/pydantic_redis/syncio/__init__.py
+++ b/pydantic_redis/syncio/__init__.py
@@ -1,6 +1,30 @@
-"""Package containing the synchronous and thus default version of pydantic_redis"""
+"""Synchronous API for pydantic-redis ORM.
+
+Typical usage example:
+
+```python
+# from pydantic_redis import Store, Model, RedisConfig
+from pydantic_redis.syncio import Store, Model, RedisConfig
+
+class Book(Model):
+    _primary_key_field = 'title'
+    title: str
+
+if __name__ == '__main__':
+    store = Store(name="sample", redis_config=RedisConfig())
+    store.register_model(Book)
+
+    Book.insert(Book(title="Oliver Twist", author="Charles Dickens"))
+    Book.update(
+        _id="Oliver Twist", data={"author": "Jane Austen"}, life_span_seconds=3600
+    )
+    results = Book.select()
+    Book.delete(ids=["Oliver Twist", "Great Expectations"])
+```
+"""
+
 from .model import Model
 from .store import Store
-from ..shared.config import RedisConfig
+from ..config import RedisConfig
 
 __all__ = [Model, Store, RedisConfig]
diff --git a/pydantic_redis/syncio/model.py b/pydantic_redis/syncio/model.py
index 25cbe545..f525d91d 100644
--- a/pydantic_redis/syncio/model.py
+++ b/pydantic_redis/syncio/model.py
@@ -1,10 +1,13 @@
-"""Module containing the model classes"""
+"""Exposes the Base `Model` class for creating custom synchronous models.
+
+This module contains the `Model` class which should be inherited when
+creating model's for use in the synchronous API of pydantic-redis
+"""
 from typing import Optional, List, Any, Union, Dict
 
-from pydantic_redis.shared.model import AbstractModel
-from pydantic_redis.shared.model.insert_utils import insert_on_pipeline
-from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key
-from pydantic_redis.shared.model.select_utils import (
+from .._shared.model import AbstractModel
+from .._shared.model.insert_utils import insert_on_pipeline
+from .._shared.model.select_utils import (
     select_all_fields_all_ids,
     select_all_fields_some_ids,
     select_some_fields_all_ids,
@@ -13,12 +16,16 @@
 )
 
 from .store import Store
-from ..shared.model.delete_utils import delete_on_pipeline
+from .._shared.model.delete_utils import delete_on_pipeline
 
 
 class Model(AbstractModel):
     """
-    The section in the store that saves rows of the same kind
+    The Base class for all Synchronous models.
+
+    Inherit this class when creating a new model.
+    The new model should have `_primary_key_field` defined.
+    Any interaction with redis is done through `Model`'s.
     """
 
     _store: Store
@@ -29,8 +36,18 @@ def insert(
         data: Union[List[AbstractModel], AbstractModel],
         life_span_seconds: Optional[float] = None,
     ):
-        """
-        Inserts a given row or sets of rows into the table
+        """Inserts a given record or list of records into the redis.
+
+        Can add a single record or multiple records into redis.
+        The records must be instances of this class. i.e. a `Book`
+        model can only insert `Book` instances.
+
+        Args:
+            data: a model instance or list of model instances to put
+                into the redis store
+            life_span_seconds: the time-to-live in seconds of the records
+                to be inserted. If not specified, it defaults to the `Store`'s
+                life_span_seconds.
         """
         store = cls.get_store()
 
@@ -62,8 +79,17 @@ def insert(
     def update(
         cls, _id: Any, data: Dict[str, Any], life_span_seconds: Optional[float] = None
     ):
-        """
-        Updates a given row or sets of rows in the table
+        """Updates the record whose primary key is `_id`.
+
+        Updates the record of this Model in redis whose primary key is equal to the `_id` provided.
+        The record is partially updated from the `data`.
+        If `life_span_seconds` is provided, it will also update the time-to-live of
+        the record.
+
+        Args:
+            _id: the primary key of record to be updated.
+            data: the new changes
+            life_span_seconds: the new time-to-live for the record
         """
         store = cls.get_store()
         life_span = (
@@ -85,8 +111,13 @@ def update(
 
     @classmethod
     def delete(cls, ids: Union[Any, List[Any]]):
-        """
-        deletes a given row or sets of rows in the table
+        """Removes a list of this Model's records from redis
+
+        Removes all the records for the current Model whose primary keys
+        have been included in the `ids` passed.
+
+        Args:
+            ids: list of primary keys of the records to remove
         """
         store = cls.get_store()
         with store.redis_store.pipeline() as pipeline:
@@ -101,16 +132,31 @@ def select(
         skip: int = 0,
         limit: Optional[int] = None,
         **kwargs,
-    ):
-        """
-        Selects given rows or sets of rows in the table
+    ) -> Union["Model", Dict[str, Any]]:
+        """Retrieves records of this Model from redis.
+
+        Retrieves the records for this Model from redis.
+
+        Args:
+            columns: the fields to return for each record
+            ids: the primary keys of the records to returns
+            skip: the number of records to skip. (default: 0)
+            limit: the maximum number of records to return
+
+        Returns:
+            By default, it returns all records that belong to current Model.
+
+            If `ids` are specified, it returns only records whose primary keys
+            have been listed in `ids`.
+
+            If `skip` and `limit` are specified WITHOUT `ids`, a slice of
+            all records are returned.
 
-        However, if `limit` is set, the number of items
-        returned will be less or equal to `limit`.
-        `skip` defaults to 0. It is the number of items to skip.
-        `skip` is only relevant when limit is specified.
+            If `limit` and `ids` are specified, `limit` is ignored.
 
-        `skip` and `limit` are irrelevant when `ids` are provided.
+            If `columns` are specified, a list of dictionaries containing only
+            the fields specified in `columns` is returned. Otherwise, instances
+            of the current Model are returned.
         """
         if columns is None and ids is None:
             response = select_all_fields_all_ids(model=cls, skip=skip, limit=limit)
diff --git a/pydantic_redis/syncio/store.py b/pydantic_redis/syncio/store.py
index 0b36cc73..8cd1b71c 100644
--- a/pydantic_redis/syncio/store.py
+++ b/pydantic_redis/syncio/store.py
@@ -1,23 +1,46 @@
-"""Module containing the store classes for sync io"""
+"""Exposes the `Store` class for managing a collection of Model's.
+
+Stores represent a collection of different kinds of records saved in
+a redis database. They only expose records whose `Model`'s have been
+registered in them. Thus redis server can have multiple stores each
+have a different image of the actual data in redis.
+
+A model must be registered with a store before it can interact with
+a redis database.
+"""
 from typing import Dict, Type, TYPE_CHECKING
 
 import redis
 
-from ..shared.store import AbstractStore
+from .._shared.store import AbstractStore
 
 if TYPE_CHECKING:
     from .model import Model
 
 
 class Store(AbstractStore):
-    """
-    A store that allows a declarative way of querying for data in redis
+    """Manages a collection of Model's, connecting them to a redis database
+
+    A Model can only interact with a redis database when it is registered
+    with a `Store` that is connected to that database.
+
+    Attributes:
+        models (Dict[str, Type[pydantic_redis.syncio.Model]]): a mapping of registered `Model`'s, with the keys being the
+            Model name
+        name (str): the name of this Store
+        redis_config (pydantic_redis.syncio.RedisConfig): the configuration for connecting to a redis database
+        redis_store (Optional[redis.Redis]): an Redis instance associated with this store (default: None)
+        life_span_in_seconds (Optional[int]): the default time-to-live for the records inserted in this store
+            (default: None)
     """
 
     models: Dict[str, Type["Model"]] = {}
 
     def _connect_to_redis(self) -> redis.Redis:
-        """Connects the store to redis, returning a proper connection"""
+        """Connects the store to redis.
+
+        See base class.
+        """
         return redis.from_url(
             self.redis_config.redis_url,
             encoding=self.redis_config.encoding,
diff --git a/requirements.txt b/requirements.txt
index 6e52ed5f..d023eccf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,7 @@ black==22.8.0
 pre-commit
 build
 pytest-asyncio
+mkdocs
+mkdocstrings
+mkdocs-material
+mdx_include


Test patch:
diff --git a/test/test_async_pydantic_redis.py b/test/test_async_pydantic_redis.py
index 4208075f..0d8e3a1e 100644
--- a/test/test_async_pydantic_redis.py
+++ b/test/test_async_pydantic_redis.py
@@ -4,8 +4,8 @@
 
 import pytest
 
-from pydantic_redis.shared.model.prop_utils import NESTED_MODEL_PREFIX
-from pydantic_redis.shared.utils import strip_leading
+from pydantic_redis._shared.model.prop_utils import NESTED_MODEL_PREFIX  # noqa
+from pydantic_redis._shared.utils import strip_leading  # noqa
 from pydantic_redis.asyncio import Model, RedisConfig
 from test.conftest import (
     async_redis_store_fixture,
diff --git a/test/test_pydantic_redis.py b/test/test_pydantic_redis.py
index a3715769..af5eec7f 100644
--- a/test/test_pydantic_redis.py
+++ b/test/test_pydantic_redis.py
@@ -4,9 +4,9 @@
 
 import pytest
 
-from pydantic_redis.shared.config import RedisConfig
-from pydantic_redis.shared.model.prop_utils import NESTED_MODEL_PREFIX
-from pydantic_redis.shared.utils import strip_leading
+from pydantic_redis.config import RedisConfig  # noqa
+from pydantic_redis._shared.model.prop_utils import NESTED_MODEL_PREFIX  # noqa
+from pydantic_redis._shared.utils import strip_leading  # noqa
 from pydantic_redis.syncio.model import Model
 from test.conftest import (
     redis_store_fixture,