diff --git a/Pipfile b/Pipfile
index 921318cd0001bcf6253b419eb8fa30d2c50a375f..0b6c797572887983771813753c4b8c71b29d143c 100644
--- a/Pipfile
+++ b/Pipfile
@@ -15,18 +15,16 @@ numpy = "*"
 scipy = "*"
 obspy = ">1.2.0"
 pycurl = "*"
-
 authlib = "*"
 cryptography = "*"
-databases = {extras = ["postgresql", "sqlite"],version = "*"}
+databases = {extras = ["sqlite"],version = "*"}
 fastapi = "*"
 httpx = "==0.11.1"
 openpyxl = "*"
-orm = "==0.1.5"
 pydantic = "==1.4"
 sqlalchemy = "*"
+sqlalchemy-utc = "*"
 uvicorn = "*"
-typesystem = "==0.2.4"
 
 [pipenv]
 allow_prereleases = true
diff --git a/Pipfile.lock b/Pipfile.lock
index 6f8dbed1be00f10269a18049ff3541d018dacfcf..c91debcbc9a19b055515563ea50d802f293a4e37 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "9d11130b39f605d10cd4d32c8b721e21b38cdbae4b5eaf19f794d6181184a08c"
+            "sha256": "feb945aefc3117570ac55b98510d96243c861469466ee798fa8b49416693f89d"
         },
         "pipfile-spec": 6,
         "requires": {},
@@ -201,10 +201,10 @@
         },
         "hstspreload": {
             "hashes": [
-                "sha256:3d2a71e189aa9216e92130865d907a4be4fd5db6df79bfa6e843e72f86a05aef",
-                "sha256:3e952e761ea9c43ccf1c61c2b1962da63c66c85469b18608228f95fcae6535ee"
+                "sha256:acbf4d6fd362b363ce567db56a8667c4f0e43073001add9c4e43f449c11e1d81",
+                "sha256:ca0af58f803598a737d5f325212e488b24de1ac9f0cf6fa12da1a3f4651914e6"
             ],
-            "version": "==2020.4.7"
+            "version": "==2020.4.14"
         },
         "httptools": {
             "hashes": [
@@ -376,13 +376,6 @@
             "index": "pypi",
             "version": "==3.0.3"
         },
-        "orm": {
-            "hashes": [
-                "sha256:37cb4757b670c1713f4e0d65874c5afe819acbd712abb9743c97e1d4b00d511c"
-            ],
-            "index": "pypi",
-            "version": "==0.1.5"
-        },
         "psycopg2-binary": {
             "hashes": [
                 "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac",
@@ -531,11 +524,37 @@
         },
         "sqlalchemy": {
             "hashes": [
-                "sha256:7224e126c00b8178dfd227bc337ba5e754b197a3867d33b9f30dc0208f773d70"
+                "sha256:083e383a1dca8384d0ea6378bd182d83c600ed4ff4ec8247d3b2442cf70db1ad",
+                "sha256:0a690a6486658d03cc6a73536d46e796b6570ac1f8a7ec133f9e28c448b69828",
+                "sha256:114b6ace30001f056e944cebd46daef38fdb41ebb98f5e5940241a03ed6cad43",
+                "sha256:128f6179325f7597a46403dde0bf148478f868df44841348dfc8d158e00db1f9",
+                "sha256:13d48cd8b925b6893a4e59b2dfb3e59a5204fd8c98289aad353af78bd214db49",
+                "sha256:211a1ce7e825f7142121144bac76f53ac28b12172716a710f4bf3eab477e730b",
+                "sha256:2dc57ee80b76813759cccd1a7affedf9c4dbe5b065a91fb6092c9d8151d66078",
+                "sha256:3e625e283eecc15aee5b1ef77203bfb542563fa4a9aa622c7643c7b55438ff49",
+                "sha256:43078c7ec0457387c79b8d52fff90a7ad352ca4c7aa841c366238c3e2cf52fdf",
+                "sha256:5b1bf3c2c2dca738235ce08079783ef04f1a7fc5b21cf24adaae77f2da4e73c3",
+                "sha256:6056b671aeda3fc451382e52ab8a753c0d5f66ef2a5ccc8fa5ba7abd20988b4d",
+                "sha256:68d78cf4a9dfade2e6cf57c4be19f7b82ed66e67dacf93b32bb390c9bed12749",
+                "sha256:7025c639ce7e170db845e94006cf5f404e243e6fc00d6c86fa19e8ad8d411880",
+                "sha256:7224e126c00b8178dfd227bc337ba5e754b197a3867d33b9f30dc0208f773d70",
+                "sha256:7d98e0785c4cd7ae30b4a451416db71f5724a1839025544b4edbd92e00b91f0f",
+                "sha256:8d8c21e9d4efef01351bf28513648ceb988031be4159745a7ad1b3e28c8ff68a",
+                "sha256:bbb545da054e6297242a1bb1ba88e7a8ffb679f518258d66798ec712b82e4e07",
+                "sha256:d00b393f05dbd4ecd65c989b7f5a81110eae4baea7a6a4cdd94c20a908d1456e",
+                "sha256:e18752cecaef61031252ca72031d4d6247b3212ebb84748fc5d1a0d2029c23ea"
             ],
             "index": "pypi",
             "version": "==1.3.16"
         },
+        "sqlalchemy-utc": {
+            "hashes": [
+                "sha256:ec5395dfa4d237239c162a1b83283d88c8e2a94219512708634c55329c900278",
+                "sha256:fed53af37d250168b99eba8f9908a50e34e10dab3c32d38df3e65601ac951baf"
+            ],
+            "index": "pypi",
+            "version": "==0.10.0"
+        },
         "starlette": {
             "hashes": [
                 "sha256:6169ee78ded501095d1dda7b141a1dc9f9934d37ad23196e180150ace2c6449b",
@@ -543,13 +562,6 @@
             ],
             "version": "==0.13.2"
         },
-        "typesystem": {
-            "hashes": [
-                "sha256:ba2bd10f1c5844d08dd8841e777bdee55bfca569bf21cb96cd0f91e0a4f66cd8"
-            ],
-            "index": "pypi",
-            "version": "==0.2.4"
-        },
         "urllib3": {
             "hashes": [
                 "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
@@ -655,39 +667,39 @@
         },
         "coverage": {
             "hashes": [
-                "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0",
-                "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30",
-                "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b",
-                "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0",
-                "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823",
-                "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe",
-                "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037",
-                "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6",
-                "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31",
-                "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd",
-                "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892",
-                "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1",
-                "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78",
-                "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac",
-                "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006",
-                "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014",
-                "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2",
-                "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7",
-                "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8",
-                "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7",
-                "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9",
-                "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1",
-                "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307",
-                "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a",
-                "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435",
-                "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0",
-                "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5",
-                "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441",
-                "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732",
-                "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de",
-                "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"
-            ],
-            "version": "==5.0.4"
+                "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
+                "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
+                "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
+                "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
+                "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
+                "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
+                "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
+                "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
+                "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
+                "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
+                "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
+                "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
+                "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
+                "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
+                "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
+                "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
+                "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
+                "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
+                "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
+                "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
+                "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
+                "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
+                "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
+                "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
+                "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
+                "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
+                "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
+                "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
+                "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
+                "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
+                "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
+            ],
+            "version": "==5.1"
         },
         "distlib": {
             "hashes": [
diff --git a/geomagio/api/db/create.py b/geomagio/api/db/create.py
index dbabda419503e88df557422a6c09b54d65da3db2..b2dc79313aa132b8bc510a87cc5900e1e60d6d36 100644
--- a/geomagio/api/db/create.py
+++ b/geomagio/api/db/create.py
@@ -3,7 +3,8 @@ import sqlalchemy
 from .common import database, sqlalchemy_metadata
 
 # register models with sqlalchemy_metadata by importing
-from .session import Session
+from .metadata import metadata
+from .session import session
 
 
 def create_db():
diff --git a/geomagio/api/db/metadata.py b/geomagio/api/db/metadata.py
index 384d970e0a7ccf5d8bb7c89e7cdd39e0cdc943cf..79d4b9a35e2e15b421300c102401b8b8611478ad 100644
--- a/geomagio/api/db/metadata.py
+++ b/geomagio/api/db/metadata.py
@@ -1,93 +1,112 @@
 import datetime
 import enum
 
-import orm
-
+from obspy import UTCDateTime
+from sqlalchemy import or_, Boolean, Column, Index, Integer, String, Table, Text
+import sqlalchemy_utc
 
+from ...metadata import Metadata, MetadataCategory
 from .common import database, sqlalchemy_metadata
 
 
-# known category values as enumeration
-class MetadataCategory(str, enum.Enum):
-    ADJUSTED_MATRIX = "adjusted-matrix"
-    FLAG = "flag"
-    READING = "reading"
-
-
-class Metadata(orm.Model):
-    """Metadata database model.
-
-    This class is used for Data flagging and other Metadata.
-
-    Flag example:
-    ```
-    automatic_flag = Metadata(
-        created_by = 'algorithm/version',
-        start_time = UTCDateTime('2020-01-02T00:17:00.1Z'),
-        end_time = UTCDateTime('2020-01-02T00:17:00.1Z'),
-        network = 'NT',
-        station = 'BOU',
-        channel = 'BEU',
-        category = CATEGORY_FLAG,
-        comment = "spike detected",
-        priority = 1,
-        data_valid = False)
-    ```
-
-    Adjusted Matrix example:
-    ```
-    adjusted_matrix = Metadata(
-        created_by = 'algorithm/version',
-        start_time = UTCDateTime('2020-01-02T00:17:00Z'),
-        end_time = None,
-        network = 'NT',
-        station = 'BOU',
-        category = CATEGORY_ADJUSTED_MATRIX,
-        comment = 'automatic adjusted matrix',
-        priority = 1,
-        value = {
-            'parameters': {'x': 1, 'y': 2, 'z': 3}
-            'matrix': [ ... ]
-        }
-    )
-    ```
-    """
-
-    __tablename__ = "metadata"
-    __database__ = database
-    __metadata__ = sqlalchemy_metadata
-
-    id = orm.Integer(primary_key=True)
+"""Metadata database model.
 
+See pydantic model geomagio.metadata.Metadata
+"""
+metadata = Table(
+    "metadata",
+    sqlalchemy_metadata,
+    ## COLUMNS
+    Column("id", Integer, primary_key=True),
     # author
-    created_by = orm.Text(index=True)
-    created_time = orm.DateTime(default=datetime.datetime.utcnow, index=True)
+    Column("created_by", String(length=255), index=True),
+    Column(
+        "created_time",
+        sqlalchemy_utc.UtcDateTime,
+        default=sqlalchemy_utc.now,
+        index=True,
+    ),
     # reviewer
-    reviewed_by = orm.Text(allow_null=True, index=True)
-    reviewed_time = orm.DateTime(allow_null=True, index=True)
-
+    Column("reviewed_by", String(length=255), index=True, nullable=True),
+    Column("reviewed_time", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
     # time range
-    starttime = orm.DateTime(allow_null=True, index=True)
-    endtime = orm.DateTime(allow_null=True, index=True)
-    # what metadata applies to
-    # channel/location allow_null for wildcard
-    network = orm.String(index=True, max_length=255)
-    station = orm.String(index=True, max_length=255)
-    channel = orm.String(allow_null=True, index=True, max_length=255)
-    location = orm.String(allow_null=True, index=True, max_length=255)
-
+    Column("starttime", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
+    Column("endtime", sqlalchemy_utc.UtcDateTime, index=True, nullable=True),
+    # what data metadata references, null for wildcard
+    Column("network", String(length=255), nullable=True),  # indexed below
+    Column("station", String(length=255), nullable=True),  # indexed below
+    Column("channel", String(length=255), nullable=True),  # indexed below
+    Column("location", String(length=255), nullable=True),  # indexed below
     # category (flag, matrix, etc)
-    category = orm.String(index=True, max_length=255)
+    Column("category", String(length=255)),  # indexed below
     # higher priority overrides lower priority
-    priority = orm.Integer(default=1, index=True)
+    Column("priority", Integer, default=1),
     # whether data is valid (primarily for flags)
-    data_valid = orm.Boolean(default=True, index=True)
-    # value
-    metadata = orm.JSON(allow_null=True)
+    Column("data_valid", Boolean, default=True, index=True),
     # whether metadata is valid (based on review)
-    metadata_valid = orm.Boolean(default=True, index=True)
-
+    Column("metadata_valid", Boolean, default=True, index=True),
+    # metadata json blob
+    Column("metadata", Text, nullable=True),
     # general comment
-    comment = orm.Text(allow_null=True)
+    Column("comment", Text, nullable=True),
     # review specific comment
-    review_comment = orm.Text(allow_null=True)
+    Column("review_comment", Text, nullable=True),
+    ## INDICES
+    Index(
+        "index_station_metadata",
+        # sncl
+        "network",
+        "station",
+        "channel",
+        "location",
+        # type
+        "category",
+        # date
+        "starttime",
+        "endtime",
+        # valid
+        "metadata_valid",
+        "data_valid",
+    ),
+    Index(
+        "index_category_time",
+        # type
+        "category",
+        # date
+        "starttime",
+        "endtime",
+    ),
+)
+
+
+async def get_metadata(
+    *,  # make all params keyword
+    network: str,
+    station: str,
+    channel: str = None,
+    location: str = None,
+    category: MetadataCategory = None,
+    starttime: UTCDateTime = None,
+    endtime: UTCDateTime = None,
+    data_valid: bool = None,
+    metadata_valid: bool = None,
+):
+    query = (
+        metadata.select()
+        .where(metadata.c.network.like(network or "%"))
+        .where(metadata.c.station.like(station or "%"))
+        .where(metadata.c.channel.like(channel or "%"))
+        .where(metadata.c.location.like(location or "%"))
+    )
+    if starttime:
+        query = query.where(
+            or_(metadata.c.endtime == None, metadata.c.endtime > starttime.datetime)
+        )
+    if endtime:
+        query = query.where(
+            or_(metadata.c.starttime == None, metadata.c.starttime < endtime.datetime)
+        )
+    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)
diff --git a/geomagio/api/db/session.py b/geomagio/api/db/session.py
index f5a94fcaea153df3f40d109695975236bdd9d215..f5a3f89e4045b56b8dc7673c64f4014b04a77847 100644
--- a/geomagio/api/db/session.py
+++ b/geomagio/api/db/session.py
@@ -1,51 +1,52 @@
-import datetime
+from datetime import datetime, timedelta, timezone
 import json
 from typing import Dict, Optional
 
+import sqlalchemy
+import sqlalchemy_utc
 
-import orm
 from .common import database, sqlalchemy_metadata
 
 
-class Session(orm.Model):
-    """Model for database sessions.
-    """
+session = sqlalchemy.Table(
+    "session",
+    sqlalchemy_metadata,
+    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
+    sqlalchemy.Column("session_id", sqlalchemy.String(length=100), index=True),
+    sqlalchemy.Column("data", sqlalchemy.Text),
+    sqlalchemy.Column("updated", sqlalchemy_utc.UtcDateTime, index=True),
+)
 
-    __tablename__ = "session"
-    __database__ = database
-    __metadata__ = sqlalchemy_metadata
 
-    id = orm.Integer(primary_key=True)
-    session_id = orm.String(index=True, max_length=100)
-    data = orm.Text()
-    updated = orm.DateTime(index=True)
-
-
-async def delete_session(session_id: str):
-    try:
-        session = await Session.objects.get(session_id=session_id)
-        await session.delete()
-    except orm.exceptions.NoMatch:
-        return {}
+async def delete_session(session_id: str) -> None:
+    query = session.delete().where(session.c.session_id == session_id)
+    await database.execute(query)
 
 
 async def get_session(session_id: str) -> str:
-    try:
-        session = await Session.objects.get(session_id=session_id)
-        return session.data
-    except orm.exceptions.NoMatch:
-        return {}
-
-
-async def remove_expired_sessions(max_age: datetime.timedelta):
-    now = datetime.datetime.now(tz=datetime.timezone.utc)
-    await Session.objects.delete(updated__lt=(now - max_age))
-
-
-async def save_session(session_id: str, data: str):
-    updated = datetime.datetime.now(tz=datetime.timezone.utc)
-    try:
-        session = await Session.objects.get(session_id=session_id)
-        await session.update(data=data, updated=updated)
-    except orm.exceptions.NoMatch:
-        await Session.objects.create(session_id=session_id, data=data, updated=updated)
+    query = session.select().where(session.c.session_id == session_id)
+    row = await database.fetch_one(query)
+    return row.data
+
+
+async def remove_expired_sessions(max_age: timedelta) -> None:
+    threshold = datetime.now(tz=timezone.utc) - max_age
+    query = session.delete().where(session.c.updated < threshold)
+    await database.execute(query)
+
+
+async def save_session(session_id: str, data: str) -> None:
+    updated = datetime.now(tz=timezone.utc)
+    # try update first
+    query = (
+        session.update()
+        .where(session.c.session_id == session_id)
+        .values(data=data, updated=updated)
+    )
+    count = await database.execute(query)
+    if count == 0:
+        # no matching session, insert
+        query = session.insert().values(
+            session_id=session_id, data=data, updated=updated
+        )
+        await database.execute(query)
diff --git a/geomagio/metadata/Metadata.py b/geomagio/metadata/Metadata.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1962fdd243562debc2d844d340d6375799aeb59
--- /dev/null
+++ b/geomagio/metadata/Metadata.py
@@ -0,0 +1,77 @@
+from typing import Dict
+
+from obspy import UTCDateTime
+from pydantic import BaseModel
+
+from .. import pydantic_utcdatetime
+from .MetadataCategory import MetadataCategory
+
+
+class Metadata(BaseModel):
+    """
+    This class is used for Data flagging and other Metadata.
+
+    Flag example:
+    ```
+    automatic_flag = Metadata(
+        created_by = 'algorithm/version',
+        start_time = UTCDateTime('2020-01-02T00:17:00.1Z'),
+        end_time = UTCDateTime('2020-01-02T00:17:00.1Z'),
+        network = 'NT',
+        station = 'BOU',
+        channel = 'BEU',
+        category = MetadataCategory.FLAG,
+        comment = "spike detected",
+        priority = 1,
+        data_valid = False)
+    ```
+
+    Adjusted Matrix example:
+    ```
+    adjusted_matrix = Metadata(
+        created_by = 'algorithm/version',
+        start_time = UTCDateTime('2020-01-02T00:17:00Z'),
+        end_time = None,
+        network = 'NT',
+        station = 'BOU',
+        category = MetadataCategory.ADJUSTED_MATRIX,
+        comment = 'automatic adjusted matrix',
+        priority = 1,
+        value = {
+            'parameters': {'x': 1, 'y': 2, 'z': 3}
+            'matrix': [ ... ]
+        }
+    )
+    ```
+    """
+
+    # database id
+    id: int = None
+    # author
+    created_by: str = None
+    created_time: UTCDateTime = None
+    # reviewer
+    reviewed_by: str = None
+    reviewed_time: UTCDateTime = None
+    # time range
+    starttime: UTCDateTime = None
+    endtime: UTCDateTime = None
+    # what data metadata references, null for wildcard
+    network: str = None
+    station: str = None
+    channel: str = None
+    location: str = None
+    # category (flag, matrix, etc)
+    category: MetadataCategory = None
+    # higher priority overrides lower priority
+    priority: int = 1
+    # whether data is valid (primarily for flags)
+    data_valid: bool = True
+    # whether metadata is valid (based on review)
+    metadata_valid: bool = True
+    # metadata json blob
+    metadata: Dict = None
+    # general comment
+    comment: str = None
+    # review specific comment
+    review_comment: str = None
diff --git a/geomagio/metadata/MetadataCategory.py b/geomagio/metadata/MetadataCategory.py
new file mode 100644
index 0000000000000000000000000000000000000000..d70092f842e83753b5121c7ea093cabcc2cfd36b
--- /dev/null
+++ b/geomagio/metadata/MetadataCategory.py
@@ -0,0 +1,8 @@
+from enum import Enum
+
+# known category values as enumeration
+class MetadataCategory(str, Enum):
+    ADJUSTED_MATRIX = "adjusted-matrix"
+    FLAG = "flag"
+    INSTRUMENT = "instrument"
+    READING = "reading"