diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2969886e8c94b722c1a1d09fa28a81beb0ccd663
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.venv
+*.pyc
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..ffb278e557a0583f626dd93e57d8a21c8cffc600
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+FROM python:3.6-slim
+
+RUN apt-get update
+RUN apt-get install -y libpq-dev python-dev gcc
+
+## make a local directory
+RUN mkdir /app
+
+# set "app" as the working directory from which CMD, RUN, ADD references
+WORKDIR /app
+
+# copy requirements.txt to /app
+ADD requirements.txt .
+
+# required to be able to install old MarkupSafe==1.0.0 version
+RUN pip install --upgrade pip setuptools==45.2.0
+
+# pip install the local requirements.txt
+RUN pip install -r requirements.txt
+
+# now copy all the files in this directory to /code
+ADD . .
+
+# Listen to port 80 at runtime
+EXPOSE 5000
+
+# Define our command to be run when launching the container
+CMD ["gunicorn", "app:app", "-b", "0.0.0.0:5000", "--workers", "4", "--reload", "--capture-output", "--enable-stdio-inheritance", "--log-level", "DEBUG"]
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cd2077dc9ee9e4c850d0ea24c6da3c8c16f7391
--- /dev/null
+++ b/api/__init__.py
@@ -0,0 +1,3 @@
+from flask import Blueprint
+
+api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1')
diff --git a/api/apps.py b/api/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..af9bcd6e2217a68f5c857501a1aedc6ef774a6e9
--- /dev/null
+++ b/api/apps.py
@@ -0,0 +1,66 @@
+from flask import jsonify
+from flask_jwt_extended import jwt_required
+from flask_cors import cross_origin
+
+from . import api_v1
+
+CONFIG_DATA = [
+    {
+        "id": "values.yml",
+        "description": "Some user friendly description",
+        "raw": "cronjob:\n  # Set curl to accept insecure connections when acme staging is used\n  curlInsecure: false",
+        "fields": [
+            {"name": "cronjob", "type": "string", "value": ""},
+            {"name": "curlInsecure", "type": "boolean", "value": "false"}
+        ]
+    }
+]
+
+APPS_DATA = [
+    {"id": 1, "name": "Nextcloud", "enabled": True, "status": "ON for everyone"},
+    {"id": 2, "name": "Rocketchat", "enabled": True, "status": "ON for everyone"},
+    {"id": 3, "name": "Wordpress", "enabled": False, "status": "ON for everyone"}
+]
+
+APP_DATA = {"id": 1, "name": "Nextcloud", "selected": True, "status": "ON for everyone", "config": CONFIG_DATA},
+
+
+@api_v1.route('/apps', methods=['GET'])
+@jwt_required()
+@cross_origin()
+def get_apps():
+    return jsonify(APPS_DATA)
+
+
+@api_v1.route('/apps/<string:slug>', methods=['GET'])
+@jwt_required()
+def get_app(slug):
+    return jsonify(APPS_DATA[0])
+
+
+@api_v1.route('/apps', methods=['POST'])
+@jwt_required()
+@cross_origin()
+def post_app():
+    return jsonify(APPS_DATA), 201
+
+
+@api_v1.route('/apps/<string:slug>', methods=['PUT'])
+@jwt_required()
+@cross_origin()
+def put_app(slug):
+    return jsonify(APPS_DATA)
+
+
+@api_v1.route('/apps/<string:slug>/config', methods=['GET'])
+@jwt_required()
+@cross_origin()
+def get_config(slug):
+    return jsonify(CONFIG_DATA)
+
+
+@api_v1.route('/apps/<string:slug>/config', methods=['DELETE'])
+@jwt_required()
+@cross_origin()
+def delete_config(slug):
+    return jsonify(CONFIG_DATA)
diff --git a/api/auth.py b/api/auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb4cdd9d1742e19f3aa90eb9654d92ea4b9699aa
--- /dev/null
+++ b/api/auth.py
@@ -0,0 +1,21 @@
+from flask import request, jsonify
+from flask_jwt_extended import create_access_token
+from flask_cors import cross_origin
+
+from . import api_v1
+
+USERNAME = 'admin'
+PASSWORD = 'admin'
+
+
+@api_v1.route('/login', methods=['POST'])
+@cross_origin()
+def login():
+    username = request.json.get('username')
+    password = request.json.get('password')
+
+    if username != USERNAME or password != PASSWORD:
+        return jsonify({'errorMessage': 'Invalid username or password'}), 401
+
+    access_token = create_access_token(identity=username)
+    return jsonify({'username': USERNAME, 'access_token': access_token})
diff --git a/api/users.py b/api/users.py
new file mode 100644
index 0000000000000000000000000000000000000000..b96a03f603d7f5eeedc42df8b93f9ed26a138fef
--- /dev/null
+++ b/api/users.py
@@ -0,0 +1,38 @@
+from flask import jsonify
+from flask_jwt_extended import jwt_required
+from flask_cors import cross_origin
+
+from . import api_v1
+
+
+USER_DATA = [
+  {"id": 1, "email": "john@doe.com", "name": "John Doe", "status": "active", "last_login": "2021-08-03T07:40:51+00:00"}
+]
+
+
+@api_v1.route('/users', methods=['GET'])
+@jwt_required()
+@cross_origin()
+def get_users():
+    return jsonify(USER_DATA)
+
+
+@api_v1.route('/users', methods=['POST'])
+@jwt_required()
+@cross_origin()
+def post_user():
+    return jsonify(USER_DATA), 201
+
+
+@api_v1.route('/users/<int:id>', methods=['PUT'])
+@jwt_required()
+@cross_origin()
+def put_user(id):
+    return jsonify(USER_DATA)
+
+
+@api_v1.route('/users/<int:id>', methods=['DELETE'])
+@jwt_required()
+@cross_origin()
+def delete_user(id):
+    return jsonify(USER_DATA)
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..8654fc4bd75e26632c9bfed24ed1d7cdb7891520
--- /dev/null
+++ b/app.py
@@ -0,0 +1,27 @@
+from flask import Flask, jsonify
+from flask_jwt_extended import JWTManager
+from flask_cors import CORS, cross_origin
+
+from config import *
+
+from api import api_v1, auth, users, apps
+
+app = Flask(__name__)
+cors = CORS(app)
+app.config['SECRET_KEY'] = SECRET_KEY
+app.register_blueprint(api_v1)
+
+jwt = JWTManager(app)
+
+
+# When token is not valid or missing handler
+@jwt.invalid_token_loader
+@jwt.unauthorized_loader
+@jwt.expired_token_loader
+def expired_token_callback(*args):
+    return jsonify({'errorMessage': 'Unauthorized'}), 401
+
+
+@app.route('/')
+def index():
+    return 'Open App Stack API v1.0'
diff --git a/config.py b/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..5532012a7651fc097cd80b94de703b366ea38680
--- /dev/null
+++ b/config.py
@@ -0,0 +1,3 @@
+import os
+
+SECRET_KEY = os.environ.get('SECRET_KEY')
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ebb162e07afb106b669d59d0658d35ce0d9d7408
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,14 @@
+cffi==1.14.6
+click==8.0.1
+cryptography==3.4.7
+Flask==2.0.1
+Flask-Cors==3.0.10
+Flask-JWT-Extended==4.2.3
+gunicorn==20.1.0
+itsdangerous==2.0.1
+Jinja2==3.0.1
+MarkupSafe==2.0.1
+pycparser==2.20
+PyJWT==2.1.0
+six==1.16.0
+Werkzeug==2.0.1
diff --git a/run_app.sh b/run_app.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c025ded3cd2c295ba19e6f5b8ad32ef5b32e563f
--- /dev/null
+++ b/run_app.sh
@@ -0,0 +1,4 @@
+export FLASK_APP=app.py
+export FLASK_ENV=development
+export SECRET_KEY="e38hq!@0n64g@qe6)5csk41t=ljo2vllog(%k7njnm4b@kh42c"
+flask run