diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..79c3c361a394cc3056e1c6218d01576c8b6db3c3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM python:3.9.7-slim
+
+RUN apt-get update && apt-get install --no-install-recommends -y bash nano mc
+
+ENV PYTHONUNBUFFERED 1
+
+RUN mkdir /src
+WORKDIR /src
+COPY . /src/
+RUN pip install -r requirements.txt
+
+CMD ["uvicorn", "main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000"]
+
+# build the image like this:
+# docker build -t adex-fastapi:latest .
+
+# log into the container
+# docker exec -it adex-fastapi sh
diff --git a/database/__init__.py b/database/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/database/crud.py b/database/crud.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff883d3da6db5a758630a416ed9bebb92dde27ff
--- /dev/null
+++ b/database/crud.py
@@ -0,0 +1,31 @@
+from sqlalchemy.orm import Session
+
+from .models import Star
+from utils import timeit
+
+@timeit
+def get_stars(db: Session, skip: int = 0, limit: int = 1000):
+    return db.query(Star).offset(skip).limit(limit).all()
+
+@timeit
+def get_rectangle(db: Session, ra_min: float = 0.0, ra_max: float = 1.0, dec_min: float = 0.0, dec_max: float = 1.0, j_mag: int = 10000, limit: int = 1000):
+    list = db.query(Star).filter(
+        Star.ra > ra_min,
+        Star.ra < ra_max,
+        Star.dec > dec_min,
+        Star.dec < dec_max,
+        Star.j_mag < j_mag
+    ).limit(limit).all()
+    print("retrieved "+str(len(list)) + ' stars')
+    return list
+
+@timeit
+def get_rectangle_query(stars, ra_min: float = 0.0, ra_max: float = 1.0, dec_min: float = 0.0, dec_max: float = 1.0, j_mag: int = 10000, limit: int = 1000):
+    query = stars.select().where(
+        Star.ra > ra_min,
+        Star.ra < ra_max,
+        Star.dec > dec_min,
+        Star.dec < dec_max,
+        Star.j_mag < j_mag
+    ).limit(limit)
+    return query
\ No newline at end of file
diff --git a/database/database.py b/database/database.py
new file mode 100644
index 0000000000000000000000000000000000000000..14c98a92d9275eadeb5c013b1bf574a6f3a2481c
--- /dev/null
+++ b/database/database.py
@@ -0,0 +1,39 @@
+import os
+import sqlalchemy
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+# Note that we are using databases package as it uses asyncpg
+# as an interface to talk to PostgreSQL database
+import databases
+
+# dev machine on SURFSara
+# DATABASE_URL = "postgresql://postgres:secret@145.38.187.31/ucac4"
+
+# mintbox docker
+# DATABASE_URL = "postgresql://postgres:secret@postgres-ucac4/ucac4"
+
+# mintbox
+# DATABASE_URL = "postgresql://postgres:secret@192.168.178.37/ucac4"
+
+# localhost
+# DATABASE_URL = "postgresql://postgres:postgres@localhost/ucac4"
+
+# read from environment
+DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres@localhost/ucac4')
+print('DATABASE_URL = '+DATABASE_URL)
+
+my_database = databases.Database(DATABASE_URL) # asyncpg
+metadata = sqlalchemy.MetaData()            # asyncpg
+
+engine = create_engine(
+    DATABASE_URL, pool_size=3, max_overflow=0
+)
+
+metadata.create_all(engine)                 # asyncpg
+
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+Base = declarative_base()
+
diff --git a/database/models.py b/database/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..a8e02bb67d3c6036475912bfc3eaeff1d2afaaf1
--- /dev/null
+++ b/database/models.py
@@ -0,0 +1,14 @@
+from sqlalchemy import Column, Integer, String, Float, DateTime
+
+from .database import Base
+
+# sql alchemy model
+class Star(Base):
+    __tablename__ = "stars"
+    zone = Column(Integer, index=True)
+    mpos1 = Column(Integer, primary_key=True, index=True)
+    ra = Column(Float, index=True)
+    dec = Column(Float, index=True)
+    j_mag = Column(Integer, index=True)
+    v_mag = Column(Integer, index=True)
+
diff --git a/database/schemas.py b/database/schemas.py
new file mode 100644
index 0000000000000000000000000000000000000000..b272d1627f92e05eb0137a2a4ac88b5835cf1fa5
--- /dev/null
+++ b/database/schemas.py
@@ -0,0 +1,18 @@
+from datetime import datetime
+
+from pydantic import BaseModel
+from typing import Optional
+
+# Pydantic models help you define request payload models
+# and response models in Python Class object notation
+
+class Star(BaseModel):
+    zone: int
+    mpos1: int
+    ra: float
+    dec: float
+    j_mag : Optional[float]
+    v_mag : Optional[float]
+
+    class Config:
+        orm_mode = True
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd6548cab01c0e233e7cad5119aff84b6dba9171
--- /dev/null
+++ b/main.py
@@ -0,0 +1,42 @@
+import uvicorn
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from database.database import my_database
+
+from routers import stars
+
+# https://fastapi.tiangolo.com/tutorial/sql-databases/
+
+app = FastAPI(
+    title="ADEX backend",
+    description="ADEX backend FastAPI",
+    version="0.0.1",
+    contact={
+        "name": "Nico Vermaas",
+        "email": "vermaas@astron.nl",
+    },
+    license_info={
+        "name": "Apache 2.0",
+        "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
+    },)
+
+app.include_router(stars.router)
+
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+@app.on_event("startup")
+async def startup():
+    await my_database.connect()
+
+@app.on_event("shutdown")
+async def shutdown():
+    await my_database.disconnect()
+
+if __name__ == "__main__":
+    uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0722f0fe9eb465af0ada6c205d143f95c1297dc1
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,17 @@
+anyio==3.6.1
+asyncpg==0.26.0
+click==8.1.3
+colorama==0.4.5
+databases==0.6.1
+fastapi==0.79.0
+greenlet==1.1.2
+gunicorn==20.1.0
+h11==0.13.0
+idna==3.3
+pydantic==1.9.2
+sniffio==1.2.0
+SQLAlchemy==1.4.40
+starlette==0.19.1
+typing_extensions==4.3.0
+uvicorn==0.18.2
+psycopg2-binary==2.9.3
diff --git a/routers/__init__.py b/routers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/routers/stars.py b/routers/stars.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5087d6b1b9697aa8c59e7ca74bf553b9d4aa031
--- /dev/null
+++ b/routers/stars.py
@@ -0,0 +1,43 @@
+from typing import List
+
+from fastapi import APIRouter, Query, Depends
+from sqlalchemy.orm import Session
+from utils import timeit
+from database import crud, models, schemas
+from database.database import SessionLocal, engine
+from database.database import my_database
+from database.models import Star
+
+router = APIRouter(tags=["stars"],)
+
+# Dependency
+def get_db():
+    db = SessionLocal()
+    try:
+        yield db
+    finally:
+        db.close()
+
+
+# http://127.0.0.1:8000/stars/
+# http://127.0.0.1:8000/stars/?skip=100&limit=100
+@router.get("/stars/", tags=["stars"], response_model=List[schemas.Star])
+async def get_stars(skip: int = 0, limit: int = 1000, db: Session = Depends(get_db)):
+    items = crud.get_stars(db, skip=skip, limit=limit)
+    return items
+
+@router.get("/stars_rectangle/", tags=["stars"], response_model=List[schemas.Star])
+async def get_stars_rectangle(ra_min: float = 0.0, ra_max: float = 1.0,
+                              dec_min: float = 0.0, dec_max: float = 1.0,
+                              j_mag: int = 10000, limit: int = 1000, db: Session = Depends(get_db)):
+    items = crud.get_rectangle(db, ra_min=ra_min, ra_max=ra_max, dec_min=dec_min, dec_max=dec_max, j_mag=j_mag, limit=limit)
+    return items
+
+@timeit
+@router.get("/stars_rectangle_async/", tags=["stars"], response_model=List[schemas.Star])
+async def get_stars_rectangle_async(ra_min: float = 0.0, ra_max: float = 1.0,
+                              dec_min: float = 0.0, dec_max: float = 1.0,
+                              j_mag: int = 10000, limit: int = 1000):
+
+    query = crud.get_rectangle_query(Star.__table__, ra_min=ra_min, ra_max=ra_max, dec_min=dec_min, dec_max=dec_max, j_mag=j_mag, limit=limit)
+    return await my_database.fetch_all(query)
\ No newline at end of file
diff --git a/run.bat b/run.bat
new file mode 100644
index 0000000000000000000000000000000000000000..76ea2dfabb42ced29be00701422d3be1adb8f576
--- /dev/null
+++ b/run.bat
@@ -0,0 +1 @@
+uvicorn main:app --reload
\ No newline at end of file
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..7aa0aa258040959cff4e562ff906a53877d11ff0
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,23 @@
+"""
+common helper functions
+"""
+import logging;
+from datetime import *
+import time
+
+logger = logging.getLogger(__name__)
+
+# this is a decorator that can be put in front (around) a function all to measure its execution time
+def timeit(method):
+    def timed(*args, **kw):
+        ts = time.time()
+        result = method(*args, **kw)
+        te = time.time()
+        if 'log_time' in kw:
+            name = kw.get('log_name', method.__name__.upper())
+            kw['log_time'][name] = int((te - ts) * 1000)
+        else:
+            print('execution time: %r  %2.2f ms' % \
+                  (method.__name__, (te - ts) * 1000))
+        return result
+    return timed
\ No newline at end of file