diff --git a/.gitignore b/.gitignore
index e7dff43ac58af825e6105802266a6918f9c56e0c..bd183f3fccddac335407db45f996930f71e5558d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 README.html
 id_*
 *.pem
-*.key
\ No newline at end of file
+*.key
+*.log
diff --git a/Application/backend/main.py b/Application/backend/main.py
index 544c803dfc84c5366e46be6e564c4d32375ae6df..4b6bff85441a7eb8eab1b1ec07a9b1b6e0a5b6ab 100644
--- a/Application/backend/main.py
+++ b/Application/backend/main.py
@@ -33,13 +33,13 @@ def handle_aws_errors(f):
 app = Flask(__name__)
 CORS(app)
 
-# Initialize S3 client
-# TODO: Add your S3 credentials in .env file
+# Initialize S3 client TODO: Add your S3 credentials as ENV variables or in
+# file '~/.aws/credentials'
 s3_client = boto3.client(
     's3',
-    aws_access_key_id=os.getenv('SWTICH_ACCESS_KEY_ID'),
+    aws_access_key_id=os.getenv('SWITCH_ACCESS_KEY_ID'),
     aws_secret_access_key=os.getenv('SWITCH_SECRET_ACCESS_KEY'),
-    endpoint_url=os.getenv('SWTICH_ENDPOINT_URL')
+    endpoint_url=os.getenv('SWITCH_ENDPOINT_URL')
 )
 
 # Constants
@@ -61,11 +61,11 @@ def enroll():
     data = request.get_json()
     email = data.get('email')
     password = data.get('password')
-    
+
     if not email or not password:
         logger.warning(f"Enrollment attempt with missing credentials: {data}")
         return jsonify({'status': 'KO:Missing credentials'}), 400
-    
+
     # TODO: Implement enrollment logic
     # 1. Check if user already exists
     # 2. Create new enrollment with timestamp
@@ -74,7 +74,7 @@ def enroll():
         'password': password,  # TODO: Hash this password in production
         'enrolled_at': datetime.utcnow().isoformat()  # Added timestamp
     }
-    
+
     return jsonify({'status': 'OK'}), 200
 
 @app.route('/login', methods=['POST'])
@@ -87,10 +87,10 @@ def login():
     data = request.get_json()
     email = data.get('email')
     password = data.get('password')
-    
+
     if not email:
         return jsonify({'status': 'KO:Missing email'}), 400
-    
+
     # TODO: Implement login logic
     # 1. Check if user exists
     # 2. Verify password
@@ -100,7 +100,7 @@ def login():
         'active': True,
         'login_at': datetime.utcnow().isoformat()  # Added timestamp
     }
-    
+
     return jsonify({'status': 'OK'}), 200
 
 # TODO: Implement these endpoints
@@ -128,4 +128,3 @@ if __name__ == '__main__':
         app.run(host='0.0.0.0', port=8000)
     except Exception as e:
         logger.critical(f"Failed to start application: {str(e)}")
-
diff --git a/Application/run.sh b/Application/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..29e8cdbb42c725e86e9bbae01c358e1d8d997304
--- /dev/null
+++ b/Application/run.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env sh
+
+[ "$WEB_SSO_DEBUG" ] && set -x
+
+web_sso_root=${WEB_SSO_ROOT:-'.'}
+
+venv_act_file="${web_sso_root}/../bin/activate"
+
+# make sure to have a slash else the 'source' command could fail
+s3_cred_env_file="${web_sso_root}/s3_credentials.env"
+
+[ -r $s3_cred_env_file ] || {
+    echo >&2 "[error] $s3_cred_env_file: missing credentials ENV file"
+    exit 1
+}
+
+source $s3_cred_env_file || exit 1
+
+# these are not really used here, just to trigger the shell's :?-test
+switch_endpoint_url=${SWITCH_ENDPOINT_URL:?'Please set ENV variable'}
+switch_access_key_id=${SWITCH_ACCESS_KEY_ID:?'Please set ENV variable'}
+switch_secret_access_key=${SWITCH_SECRET_ACCESS_KEY:?'Please set ENV variable'}
+s3_bucket_name=${S3_BUCKET_NAME:?'Please set ENV variable'}
+
+backend_pid=
+backend_log="${web_sso_root}/backend/main.log"
+backend_main="${web_sso_root}/backend/main.py"
+frontend_main="${web_sso_root}/frontend/main.py"
+
+function cleanup {
+    trap EXIT SIGINT SIGTERM
+
+    # @posix
+    ps -q $backend_pid >/dev/null && kill -TERM $backend_pid
+
+    exit $?
+}
+
+# EXIT raised on Ctrl+D in an interactive shell
+trap cleanup EXIT SIGINT SIGTERM
+
+# try to activate the venv upstream
+[ -r $venv_act_file ] && {
+    source $venv_act_file ||
+        echo >&2 "[warn] $venv_act_file: can't activated the virtual env. Going on anyway..."
+}
+
+# start the backend
+python $backend_main > $backend_log &
+sleep 2;
+backend_pid=$!
+ps -q $backend_pid >/dev/null || {
+    echo >&2 "[error] Backend crashed :-(. See log '${backend_log}'"
+    exit 1
+}
+echo >&2 -e "[ok] Backend started. Logging to file '${backend_log}'"
+
+# start the frontend -- must be explicitly terminated with Ctrl+C
+python $frontend_main
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..ecb6c8fa6924f017d4ccc28dd0dadb8de987bb7b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,129 @@
+.ONESHELL:
+
+prefix := $(HOME)
+sandbox_root := sandbox/project-web-sso
+sandbox_dir := $(prefix)/sandbox/project-web-sso
+app_sdir := Application
+application_dir := $(sandbox_dir)/$(app_sdir)
+s3_cred_file := $(application_dir)/s3_credentials.env
+pip_opts := "-r"
+frontend_port := 3000
+backend_port := 8000
+portal_url := "http://localhost:$(frontend_port)"
+terminal := xfce4-terminal
+term_opts :=
+term_title := "Web SSO application"
+
+ifeq ($(terminal), xterm)
+	term_opts += -T $(term_title) -hold
+else
+	# should catch most other emulators out there
+	term_opts += -T $(term_title) -H
+endif
+
+define _help_msg :=
+Usage:
+
+  make [command]
+
+Commands
+
+  help          ...guess what ^^
+  (v)install    install the application locally (in a Python virtual env that will be created)
+  (t)run        (open a terminal and) run the application
+
+Options
+
+  sandbox_dir   where to install the app (under '$(prefix)/'). Default: $(sandbox_dir)
+  terminal      the terminal emulator to be lunched with command 'trun'. Default: $(terminal). Supported: xterm, xfce4-terminal.
+
+Examples
+
+  Install in default sandbox -- you might need to manually install extra Python
+  dependencies:
+
+     make install
+
+  Install in a virtualenv sandbox in '/foo/bar':
+
+     make sandbox_dir=/foo/bar vinstall
+
+  Run the application in the current console:
+
+     make run
+
+  Run the application in terminal emulator 'xterm':
+
+     make terminal=xterm trun
+endef
+
+define _s3_creds :=
+SWITCH_ENDPOINT_URL='<your-s3-endpoint>'
+SWITCH_ACCESS_KEY_ID='<your-s3-access-key-id>'
+SWITCH_SECRET_ACCESS_KEY='<your-s3-secret-key>'
+S3_BUCKET_NAME='project-web-sso'
+
+export SWITCH_ENDPOINT_URL SWITCH_ENDPOINT_URL SWITCH_ACCESS_KEY_ID SWITCH_SECRET_ACCESS_KEY S3_BUCKET_NAME
+endef
+
+define echoo
+	printf "%b\n" "$*"
+endef
+
+define echoe
+	$(echoo) >&2
+endef
+
+.PHONY: help _sandbox _venv _s3cred install vinstall trun run
+
+all: help
+
+_sandbox:
+	mkdir -p $(sandbox_dir)
+
+_venv: _sandbox
+	python -m venv $(sandbox_dir) || exit 1
+
+_install: _sandbox
+	rsync -Cabhv -L --suffix=.bak $(app_sdir) $(sandbox_dir) || exit 1
+	pip install $(pip_opts) $(application_dir)/frontend/requirements.txt ||
+		exit 1
+	pip install $(pip_opts) $(application_dir)/backend/requirements.txt ||
+		exit 1
+	$(MAKE) _s3cred
+
+_s3cred:
+	[ -r $(s3_cred_file) ] && {
+		$(echoe) "[info] $(s3_cred_file): file exists. Won't touch it..."
+		exit 0
+	}
+	$(echoo) "$(_s3_creds)" > $(s3_cred_file) || exit 1
+	chmod 0600 $(s3_cred_file)
+	$(echoe) "[info] Please adapt your AWS/S3 credentials in file '$(s3_cred_file)'"
+
+install:
+	$(MAKE) pip_opts='$(pip_opts) --user' _install || exit 1
+
+vinstall: _venv
+	source $(sandbox_dir)/bin/activate
+	$(MAKE) _install
+	$(echoe) "[ok] Application installed in '$(application_dir)'"
+	$(echoe) "[info] To activate your virtual env, please, open a terminal and run:\n\
+		cd $(sandbox_dir) && source bin/activate"
+
+run:
+	export WEB_SSO_ROOT=$(application_dir)
+	$(application_dir)/run.sh
+
+trun:
+	which $(terminal) 2> /dev/null || {
+		$(echoe) "[error] $(terminal): not found. Please set your terminal program and retry:\n\tmake terminal=YOUR_TERMINAL_PROGRAM run"
+		exit 1
+	}
+	$(terminal) $(term_opts) \
+		-e "sh -c 'export WEB_SSO_ROOT=$(application_dir); \
+			$(application_dir)/run.sh'" &
+
+
+help:
+	@: $(info $(_help_msg))
diff --git a/README.md b/README.md
index 439c7ab2f43a0377608132077350d4fec78da41d..89cbb364eab8173a4da7412d05751e0bae9aaff5 100644
--- a/README.md
+++ b/README.md
@@ -320,24 +320,40 @@ The whole stack shall be redeployed whenever any of its images are updated.
 
 :construction: **To be finalized**
 
-:warning: Please respect the file layout provided by this repository!
+:bulb: Please, respect the file layout provided by this repository. Here is
+map:
+
+```
+├── Ansible
+│   ├── keys         <= SSH keys with name "id_..." (won't be committed)
+│   └── playbooks    <= recipes to be extended
+│       └── files    <= KinD/K8s files
+├── Application
+│   ├── backend      <= main.tf to be extended
+│   └── frontend
+│       └── views
+├── Docker
+└── Terraform        <= recipes to be extended
+    └── conf         <= Cloud-init files
+```
 
 You shall:
 
 1. Fork this repository.
-2. Complete the Python back-end file(s) in folder
-   `Application/backend/main.py`.
+2. Complete the Python back-end file `Application/backend/main.py`. See the
+   section [Development](#development) below.
 3. Rebuild the application back-end Docker image, and push it to your public
-   Docker Hub repositry -- **(:question: TO-DO - We should provide
+   Docker Hub repository -- **(:question: TO-DO - We should provide
    instructions)**. This task shall be automated via Ansible -- see below.
 4. Complete your Terraform files from the version you developed in
    [Lab-Terraform](https://gitedu.hesge.ch/lsds/teaching/bachelor/cloud-and-deployment/lab-terraform/-/blob/main/SwitchEngines/README.md)
    up to Task #8. Your recipe shall handle only the provisioning of the VM
    plus an S3 storage bucket -- no KinD/Kubectl package installation. Commit
-   your recipe files (included Cloud-init) and in directory `Terraform/`.
+   your recipe files and in directory `Terraform/` -- Cloud-init files are
+   already in sub-folder `conf/`.
 5. Complete your Ansible playbook, starting from the version you developed in
    [Lab-Ansible](https://gitedu.hesge.ch/lsds/teaching/bachelor/cloud-and-deployment/lab-ansible)
-   Task #10, to (commit all realted files in directory `Ansible/`):
+   Task #10, to (commit all related files in directory `Ansible/`):
     - expose the application portal's IP (i.e, the load-balancer's) to the
       Internet via `socat` or other mechanism of your choice;
     - rebuild and push the application images to your Docker Hub
@@ -367,7 +383,55 @@ You will get bonus for any of the following improvements.
     "nonce"](https://en.wikipedia.org/wiki/Cryptographic_nonce). +0.5 points.
 
 
-### Testing
+### Development
+
+#### Local installation
+
+To test your application before deploying it as a K8s service, you can install
+and run it locally in your workstation.
+
+:bulb: It is strongly recommended to install Python's "virtualenv":
+  * [virtualenv](https://virtualenv.pypa.io/en/latest/index.html), and
+  * [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/).
+
+At the top-level of your repo clone, get an overview of administrative
+commands and options:
+
+``` shell
+$ make help
+```
+
+Then, install the application in a virtual env (automatically created)
+
+``` shell
+$ make vinstall
+```
+
+Otherwise, plain install (you might need to install python dependencies
+manually):
+
+``` shell
+$ make install
+```
+
+Finally, run the application in the current console:
+
+``` shell
+$ make run
+```
+
+Or, run in another terminal:
+``` shell
+$ make trun
+```
+
+
+#### Rebuild the Docker images
+
+:construction: **need details**
+
+
+#### Test workflow
 
 The following tests shall be passed by your implementation: