diff --git a/geomagio/api/db/MetadataDatabaseFactory.py b/geomagio/api/db/MetadataDatabaseFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..748bbabc8d3f01cf78ff435b457560280840ddc0
--- /dev/null
+++ b/geomagio/api/db/MetadataDatabaseFactory.py
@@ -0,0 +1,117 @@
+from datetime import datetime
+from typing import List
+
+from databases import Database
+from obspy import UTCDateTime
+from sqlalchemy import or_
+
+from ...metadata import Metadata, MetadataCategory
+from .metadata_history_table import metadata_history
+from .metadata_table import metadata
+
+
+class MetadataDatabaseFactory(object):
+    def __init__(self, database: Database):
+        self.database = database
+
+    async def create_metadata(self, meta: Metadata) -> Metadata:
+        query = metadata.insert()
+        values = meta.datetime_dict(exclude={"id", "metadata_id"}, exclude_none=True)
+        query = query.values(**values)
+        meta.id = await self.database.execute(query)
+        return meta
+
+    async def get_metadata(
+        self,
+        *,  # make all params keyword
+        id: int = None,
+        network: str = None,
+        station: str = None,
+        channel: str = None,
+        location: str = None,
+        category: MetadataCategory = None,
+        starttime: datetime = None,
+        endtime: datetime = None,
+        created_after: datetime = None,
+        created_before: datetime = None,
+        data_valid: bool = None,
+        metadata_valid: bool = None,
+        status: str = None,
+    ):
+        query = metadata.select()
+        if id:
+            query = query.where(metadata.c.id == id)
+        if category:
+            query = query.where(metadata.c.category == category)
+        if network:
+            query = query.where(metadata.c.network == network)
+        if station:
+            query = query.where(metadata.c.station == station)
+        if channel:
+            query = query.where(metadata.c.channel.like(channel))
+        if location:
+            query = query.where(metadata.c.location.like(location))
+        if starttime:
+            query = query.where(
+                or_(
+                    metadata.c.endtime == None,
+                    metadata.c.endtime > starttime,
+                )
+            )
+        if endtime:
+            query = query.where(
+                or_(
+                    metadata.c.starttime == None,
+                    metadata.c.starttime < endtime,
+                )
+            )
+        if created_after:
+            query = query.where(metadata.c.created_time > created_after)
+        if created_before:
+            query = query.where(metadata.c.created_time < created_before)
+        if data_valid is not None:
+            query = query.where(metadata.c.data_valid == data_valid)
+        if metadata_valid is not None:
+            query = query.where(metadata.c.metadata_valid == metadata_valid)
+        if status is not None:
+            query = query.where(metadata.c.status == status)
+        rows = await self.database.fetch_all(query)
+        return [Metadata(**row) for row in rows]
+
+    async def get_metadata_by_id(self, id: int):
+        meta = await self.get_metadata(id=id)
+        if len(meta) != 1:
+            raise ValueError(f"{len(meta)} records found")
+        return meta[0]
+
+    async def get_metadata_history(self, metadata_id: int) -> List[Metadata]:
+        async with self.database.transaction() as transaction:
+            query = metadata_history.select()
+            query = query.where(metadata_history.c.metadata_id == metadata_id).order_by(
+                metadata_history.c.updated_time
+            )
+            rows = await self.database.fetch_all(query)
+            metadata = [Metadata(**row) for row in rows]
+            current_metadata = await self.get_metadata_by_id(id=metadata_id)
+            metadata.append(current_metadata)
+            # return records in order of age(newest first)
+            metadata.reverse()
+            return metadata
+
+    async def update_metadata(self, meta: Metadata, updated_by: str) -> Metadata:
+        async with self.database.transaction() as transaction:
+            # write current record to metadata history table
+            original_metadata = await self.get_metadata_by_id(id=meta.id)
+            original_metadata.metadata_id = original_metadata.id
+            values = original_metadata.datetime_dict(exclude={"id"}, exclude_none=True)
+            query = metadata_history.insert()
+            query = query.values(**values)
+            original_metadata.id = await self.database.execute(query)
+            # update record in metadata table
+            meta.updated_by = updated_by
+            meta.updated_time = UTCDateTime()
+            query = metadata.update().where(metadata.c.id == meta.id)
+            values = meta.datetime_dict(exclude={"id", "metadata_id"})
+            query = query.values(**values)
+            await self.database.execute(query)
+            return await self.get_metadata_by_id(id=meta.id)
diff --git a/geomagio/api/db/__init__.py b/geomagio/api/db/__init__.py
index 50e414c7396b72a2aa4d0b7849ba42b801ab417e..dfb22968ecc34f0a19bcc60648f7efae772bf834 100644
--- a/geomagio/api/db/__init__.py
+++ b/geomagio/api/db/__init__.py
@@ -7,5 +7,10 @@ Modules outside the api should not access the database directly.
 """
 
 from .common import database, sqlalchemy_metadata
+from .MetadataDatabaseFactory import MetadataDatabaseFactory
 
-__all__ = ["database", "sqlalchemy_metadata"]
+__all__ = [
+    "database",
+    "sqlalchemy_metadata",
+    "MetadataDatabaseFactory",
+]
diff --git a/geomagio/api/db/create.py b/geomagio/api/db/create.py
index 0ccc0a97a0d0ce5161a3850dfc0d8588ada2fdfd..144cf0840f114a2cf3440311c283ea4c655e3b8f 100644
--- a/geomagio/api/db/create.py
+++ b/geomagio/api/db/create.py
@@ -3,6 +3,7 @@ import sqlalchemy
 from .common import database, sqlalchemy_metadata
 
 # register models with sqlalchemy_metadata by importing
+from .metadata_history_table import metadata_history
 from .metadata_table import metadata
 from .session_table import session
 
diff --git a/geomagio/api/db/metadata_history_table.py b/geomagio/api/db/metadata_history_table.py
new file mode 100644
index 0000000000000000000000000000000000000000..471869fe132d16a249ca31d283c0235a8e2d10d2
--- /dev/null
+++ b/geomagio/api/db/metadata_history_table.py
@@ -0,0 +1,18 @@
+from sqlalchemy import Column, ForeignKey, Integer
+
+from .common import sqlalchemy_metadata
+from .metadata_table import metadata
+
+# create copy of original metadata table and add to sqlalchemy metadata
+metadata_history = metadata.tometadata(
+    metadata=sqlalchemy_metadata, name="metadata_history"
+)
+metadata_history.indexes.clear()
+metadata_history.append_column(
+    Column(
+        "metadata_id",
+        Integer,
+        ForeignKey("metadata.id"),
+        nullable=False,
+    ),
+)
diff --git a/geomagio/api/db/metadata_table.py b/geomagio/api/db/metadata_table.py
index 211a18044c4f675c3f659cad71ae6c2204bfbf33..e55e66878b063583136afaa8a2de152c3a37e859 100644
--- a/geomagio/api/db/metadata_table.py
+++ b/geomagio/api/db/metadata_table.py
@@ -1,10 +1,7 @@
-from datetime import datetime
-
-from sqlalchemy import or_, Boolean, Column, Index, Integer, JSON, String, Table, Text
+from sqlalchemy import Boolean, Column, Index, Integer, JSON, String, Table, Text
 import sqlalchemy_utc
 
-from ...metadata import Metadata, MetadataCategory
-from .common import database, sqlalchemy_metadata
+from .common import sqlalchemy_metadata
 
 
 """Metadata database model.
@@ -24,9 +21,9 @@ metadata = Table(
         default=sqlalchemy_utc.utcnow(),
         index=True,
     ),
-    # reviewer
-    Column("reviewed_by", String(length=255), index=True, nullable=True),
-    Column("reviewed_time", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
+    # editor
+    Column("updated_by", String(length=255), index=True, nullable=True),
+    Column("updated_time", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
     # time range
     Column("starttime", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
     Column("endtime", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
@@ -43,6 +40,8 @@ metadata = Table(
     Column("data_valid", Boolean, default=True, index=True),
     # whether metadata is valid (based on review)
     Column("metadata_valid", Boolean, default=True, index=True),
+    # deletion status indicator
+    Column("status", String(length=255), nullable=True),
     # metadata json blob
     Column("metadata", JSON, nullable=True),
     # general comment
@@ -65,6 +64,7 @@ metadata = Table(
         # valid
         "metadata_valid",
         "data_valid",
+        "status",
     ),
     Index(
         "index_category_time",
@@ -75,71 +75,3 @@ metadata = Table(
         "endtime",
     ),
 )
-
-
-async def create_metadata(meta: Metadata) -> Metadata:
-    query = metadata.insert()
-    values = meta.datetime_dict(exclude={"id"}, exclude_none=True)
-    query = query.values(**values)
-    meta.id = await database.execute(query)
-    return meta
-
-
-async def delete_metadata(id: int) -> None:
-    query = metadata.delete().where(metadata.c.id == id)
-    await database.execute(query)
-
-
-async def get_metadata(
-    *,  # make all params keyword
-    id: int = None,
-    network: str = None,
-    station: str = None,
-    channel: str = None,
-    location: str = None,
-    category: MetadataCategory = None,
-    starttime: datetime = None,
-    endtime: datetime = None,
-    created_after: datetime = None,
-    created_before: datetime = None,
-    data_valid: bool = None,
-    metadata_valid: bool = None,
-):
-    query = metadata.select()
-    if id:
-        query = query.where(metadata.c.id == id)
-    if category:
-        query = query.where(metadata.c.category == category)
-    if network:
-        query = query.where(metadata.c.network == network)
-    if station:
-        query = query.where(metadata.c.station == station)
-    if channel:
-        query = query.where(metadata.c.channel.like(channel))
-    if location:
-        query = query.where(metadata.c.location.like(location))
-    if starttime:
-        query = query.where(
-            or_(metadata.c.endtime == None, metadata.c.endtime > starttime)
-        )
-    if endtime:
-        query = query.where(
-            or_(metadata.c.starttime == None, metadata.c.starttime < endtime)
-        )
-    if created_after:
-        query = query.where(metadata.c.created_time > created_after)
-    if created_before:
-        query = query.where(metadata.c.created_time < created_before)
-    if data_valid is not None:
-        query = query.where(metadata.c.data_valid == data_valid)
-    if metadata_valid is not None:
-        query = query.where(metadata.c.metadata_valid == metadata_valid)
-    rows = await database.fetch_all(query)
-    return [Metadata(**row) for row in rows]
-
-
-async def update_metadata(meta: Metadata) -> None:
-    query = metadata.update().where(metadata.c.id == meta.id)
-    values = meta.datetime_dict(exclude={"id"})
-    query = query.values(**values)
-    await database.execute(query)
diff --git a/geomagio/api/secure/metadata.py b/geomagio/api/secure/metadata.py
index e619c66362b8b4e960dd92cba5b5d88ff493b6b2..d397faca167ba4caf279b2e90e4a4a36cc791108 100644
--- a/geomagio/api/secure/metadata.py
+++ b/geomagio/api/secure/metadata.py
@@ -21,7 +21,8 @@ from obspy import UTCDateTime
 
 from ...metadata import Metadata, MetadataCategory, MetadataQuery
 from ... import pydantic_utcdatetime
-from ..db import metadata_table
+from ..db.common import database
+from ..db import MetadataDatabaseFactory
 from .login import require_user, User
 
 # routes for login/logout
@@ -34,17 +35,12 @@ async def create_metadata(
     metadata: Metadata,
     user: User = Depends(require_user()),
 ):
-    metadata = await metadata_table.create_metadata(metadata)
+    metadata = await MetadataDatabaseFactory(database=database).create_metadata(
+        meta=metadata
+    )
     return Response(metadata.json(), status_code=201, media_type="application/json")
 
 
-@router.delete("/metadata/{id}")
-async def delete_metadata(
-    id: int, user: User = Depends(require_user([os.getenv("ADMIN_GROUP", "admin")]))
-):
-    await metadata_table.delete_metadata(id)
-
-
 @router.get("/metadata", response_model=List[Metadata])
 async def get_metadata(
     category: MetadataCategory = None,
@@ -58,6 +54,7 @@ async def get_metadata(
     location: str = None,
     data_valid: bool = None,
     metadata_valid: bool = True,
+    status: str = None,
 ):
     query = MetadataQuery(
         category=category,
@@ -71,18 +68,26 @@ async def get_metadata(
         location=location,
         data_valid=data_valid,
         metadata_valid=metadata_valid,
+        status=status,
+    )
+    metas = await MetadataDatabaseFactory(database=database).get_metadata(
+        **query.datetime_dict(exclude={"id", "metadata_id"})
     )
-    metas = await metadata_table.get_metadata(**query.datetime_dict(exclude={"id"}))
     return metas
 
 
 @router.get("/metadata/{id}", response_model=Metadata)
 async def get_metadata_by_id(id: int):
-    meta = await metadata_table.get_metadata(id=id)
-    if len(meta) != 1:
-        return Response(status_code=404)
-    else:
-        return meta[0]
+    return await MetadataDatabaseFactory(database=database).get_metadata_by_id(id=id)
+
+
+@router.get("/metadata/{metadata_id}/history", response_model=List[Metadata])
+async def get_metadata_history(
+    metadata_id: int,
+):
+    return await MetadataDatabaseFactory(database=database).get_metadata_history(
+        metadata_id=metadata_id,
+    )
 
 
 @router.put("/metadata/{id}", response_model=Metadata)
@@ -91,6 +96,7 @@ async def update_metadata(
     metadata: Metadata = Body(...),
     user: User = Depends(require_user([os.getenv("REVIEWER_GROUP", "reviewer")])),
 ):
-    await metadata_table.update_metadata(metadata)
-    # should be same, but read from database
-    return await get_metadata_by_id(metadata.id)
+    return await MetadataDatabaseFactory(database=database).update_metadata(
+        meta=metadata,
+        updated_by=user.nickname,
+    )
diff --git a/geomagio/api/ws/DataApiQuery.py b/geomagio/api/ws/DataApiQuery.py
index af1c3f73ff3fc880cc75df45cad2bc20240225f8..d955b7882a12988697232f45e72bc33dae66353d 100644
--- a/geomagio/api/ws/DataApiQuery.py
+++ b/geomagio/api/ws/DataApiQuery.py
@@ -1,7 +1,7 @@
 import datetime
 import enum
 import os
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Union
 
 from obspy import UTCDateTime
 from pydantic import BaseModel, root_validator, validator
diff --git a/geomagio/api/ws/metadata.py b/geomagio/api/ws/metadata.py
index 655c33e59f0ec84b4329e5e3c6642c6dbe24882c..1dc77ee2c7fbcbee927707629dbf60703207c3f1 100644
--- a/geomagio/api/ws/metadata.py
+++ b/geomagio/api/ws/metadata.py
@@ -4,7 +4,8 @@ from fastapi import APIRouter
 from obspy import UTCDateTime
 
 from ...metadata import Metadata, MetadataCategory, MetadataQuery
-from ..db import metadata_table
+from ..db.common import database
+from ..db import MetadataDatabaseFactory
 
 router = APIRouter()
 
@@ -20,6 +21,7 @@ async def get_metadata(
     location: str = None,
     data_valid: bool = None,
     metadata_valid: bool = True,
+    status: str = None,
 ):
     query = MetadataQuery(
         category=category,
@@ -31,6 +33,9 @@ async def get_metadata(
         location=location,
         data_valid=data_valid,
         metadata_valid=metadata_valid,
+        status=status,
+    )
+    metas = await MetadataDatabaseFactory(database=database).get_metadata(
+        **query.datetime_dict(exclude={"id"})
     )
-    metas = await metadata_table.get_metadata(**query.datetime_dict(exclude={"id"}))
     return metas
diff --git a/geomagio/metadata/Metadata.py b/geomagio/metadata/Metadata.py
index ce2ad5211ac07c5023c4821396a8abe1ed1fd4f5..dff97fbeb4d448951f0503950a77ea052ac2f851 100644
--- a/geomagio/metadata/Metadata.py
+++ b/geomagio/metadata/Metadata.py
@@ -48,12 +48,14 @@ class Metadata(BaseModel):
 
     # database id
     id: int = None
+    # metadata history id referencing database id
+    metadata_id: int = None
     # author
     created_by: str = None
     created_time: UTCDateTime = None
-    # reviewer
-    reviewed_by: str = None
-    reviewed_time: UTCDateTime = None
+    # editor
+    updated_by: str = None
+    updated_time: UTCDateTime = None
     # time range
     starttime: UTCDateTime = None
     endtime: UTCDateTime = None
@@ -76,10 +78,12 @@ class Metadata(BaseModel):
     comment: str = None
     # review specific comment
     review_comment: str = None
+    # deletion status indicator
+    status: str = None
 
     def datetime_dict(self, **kwargs):
         values = self.dict(**kwargs)
-        for key in ["created_time", "reviewed_time", "starttime", "endtime"]:
+        for key in ["created_time", "updated_time", "starttime", "endtime"]:
             if key in values and values[key] is not None:
                 values[key] = values[key].datetime.replace(tzinfo=timezone.utc)
         return values
diff --git a/geomagio/metadata/MetadataFactory.py b/geomagio/metadata/MetadataFactory.py
index 0558bcc20efa7030953ebc4ae6be0c45b700b9d5..e000c887f99b53d8f798b8d2149a1605788ea555 100644
--- a/geomagio/metadata/MetadataFactory.py
+++ b/geomagio/metadata/MetadataFactory.py
@@ -27,15 +27,6 @@ class MetadataFactory(object):
     def _get_headers(self):
         return {"Authorization": self.token} if self.token else None
 
-    def delete_metadata(self, metadata: Metadata) -> bool:
-        response = requests.delete(
-            url=f"{self.url}/{metadata.id}",
-            headers=self._get_headers(),
-        )
-        if response.status_code == 200:
-            return True
-        return False
-
     def get_metadata(self, query: MetadataQuery) -> List[Metadata]:
         if query.id:
             metadata = [self.get_metadata_by_id(id=query.id)]
diff --git a/geomagio/metadata/MetadataQuery.py b/geomagio/metadata/MetadataQuery.py
index 36e6c3ebe3cd20d880c25ff0d683757c84a2d833..5b5214849e1d3bb74821c4389052631f20c868c6 100644
--- a/geomagio/metadata/MetadataQuery.py
+++ b/geomagio/metadata/MetadataQuery.py
@@ -21,6 +21,7 @@ class MetadataQuery(BaseModel):
     location: str = None
     data_valid: Optional[bool] = None
     metadata_valid: Optional[bool] = None
+    status: Optional[str] = None
 
     def datetime_dict(self, **kwargs):
         values = self.dict(**kwargs)
diff --git a/geomagio/metadata/main.py b/geomagio/metadata/main.py
index 79cf886baa37a048046498d67b2bb785a9d21473..69e8eeecc3f6e718610547f74df5266cddd86591 100644
--- a/geomagio/metadata/main.py
+++ b/geomagio/metadata/main.py
@@ -1,7 +1,6 @@
 import sys
 import json
 import os
-import textwrap
 from typing import Dict, Optional
 
 from obspy import UTCDateTime
@@ -111,24 +110,6 @@ def create(
     print(metadata.json())
 
 
-@app.command(
-    help=f"""
-    Delete an existing metadata.
-
-    {ENVIRONMENT_VARIABLE_HELP}
-    """
-)
-def delete(
-    input_file: str,
-    url: str = GEOMAG_API_URL,
-):
-    metadata_dict = load_metadata(input_file=input_file)
-    metadata = Metadata(**metadata_dict)
-    deleted = MetadataFactory(url=url).delete_metadata(metadata=metadata)
-    if not deleted:
-        sys.exit(1)
-
-
 @app.command(
     help=f"""
     Search existing metadata.
@@ -148,6 +129,7 @@ def get(
     location: Optional[str] = None,
     metadata_valid: Optional[bool] = None,
     network: Optional[str] = None,
+    status: Optional[str] = None,
     starttime: Optional[str] = None,
     station: Optional[str] = None,
     url: str = GEOMAG_API_URL,
@@ -165,6 +147,7 @@ def get(
         network=network,
         starttime=UTCDateTime(starttime) if starttime else None,
         station=station,
+        status=status,
     )
     metadata = MetadataFactory(url=url).get_metadata(query=query)
     if getone:
diff --git a/test_metadata.py b/test_metadata.py
index 6f4eab2e8b8277a42774e2bc417c92a1fdaa7170..1a78b1bcfc571d683a27243342301bd0d8bb3558 100644
--- a/test_metadata.py
+++ b/test_metadata.py
@@ -3,7 +3,7 @@ import json
 from obspy import UTCDateTime
 
 from geomagio.adjusted import AdjustedMatrix, Metric
-from geomagio.api.db import database, metadata_table
+from geomagio.api.db import database, MetadataDatabaseFactory
 from geomagio.api.ws.Observatory import OBSERVATORIES
 from geomagio.metadata import Metadata, MetadataCategory
 from geomagio.residual import SpreadsheetAbsolutesFactory, WebAbsolutesFactory
@@ -137,7 +137,7 @@ for reading in readings:
             category=MetadataCategory.READING,
             created_by="test_metadata.py",
             network="NT",
-            reviewed_by=reviewer,
+            updated_by=reviewer,
             starttime=reading.time,
             endtime=reading.time,
             station=reading.metadata["station"],
@@ -164,14 +164,19 @@ adjusted_matrix = AdjustedMatrix(
 )
 
 test_metadata.append(
-    Metadata(station="FRD", network="NT", metadata=adjusted_matrix.dict())
+    Metadata(
+        category="adjusted-matrix",
+        station="FRD",
+        network="NT",
+        metadata=adjusted_matrix.dict(),
+    )
 )
 
 
 async def load_test_metadata():
     await database.connect()
     for meta in test_metadata:
-        await metadata_table.create_metadata(meta)
+        await MetadataDatabaseFactory(database=database).create_metadata(meta)
     await database.disconnect()