# included libs
 from typing import Any, Callable
 from json import loads as json_loads
-from os import environ, remove as os_remove
+from os import remove as os_remove
 from os.path import join as path_join
 from urllib.request import urlopen
 # non-included libs
 from scp import SCPClient  # type: ignore
 from ytplom.misc import (
         PAGE_NAMES, PATH_DB, PATH_DOWNLOADS, PATH_TEMP,
-        DatabaseConnection, PathStr, QuotaLog, VideoFile,
+        Config, DatabaseConnection, PathStr, QuotaLog, VideoFile,
         YoutubeQuery, YoutubeVideo)
 
 
-# what we might want to manually define per environs
-YTPLOM_REMOTE = environ.get('YTPLOM_REMOTE')
-YTPLOM_PORT = environ.get('YTPLOM_PORT')
-
 PATH_DB_REMOTE = PathStr(path_join(PATH_TEMP, 'remote_db.sql'))
 ATTR_NAME_LAST_UPDATE = 'last_update'
 
 
 def main():
     """Connect to remote, sync local+remote DBs, + downloads where missing."""
+    config = Config()
     ssh = SSHClient()
     ssh.load_system_host_keys()
-    ssh.connect(YTPLOM_REMOTE)
+    ssh.connect(config.remote)
     scp = SCPClient(ssh.get_transport())
     scp.get(PATH_DB, PATH_DB_REMOTE)
     local_db = DatabaseConnection(PATH_DB)
     remote_db.commit_close()
     scp.put(PATH_DB_REMOTE, PATH_DB)
     os_remove(PATH_DB_REMOTE)
-    for host, direction, mover in ((YTPLOM_REMOTE, 'local->remote', scp.put),
-                                   ('localhost', 'remote->local', scp.get)):
-        url_missing = f'http://{host}:{YTPLOM_PORT}/{PAGE_NAMES["missing"]}'
+    for host, port, direction, mover in (
+            (config.remote, config.port_remote, 'local->remote', scp.put),
+            (config.host, config.port, 'remote->local', scp.get)):
+        url_missing = f'http://{host}:{port}/{PAGE_NAMES["missing"]}'
         with urlopen(url_missing) as response:
             missing = json_loads(response.read())
         for path in (path_join(PATH_DOWNLOADS, path) for path in missing):
 
 from random import shuffle
 from time import time, sleep
 from datetime import datetime, timedelta
-from json import dumps as json_dumps
+from json import dumps as json_dumps, load as json_load
 from uuid import uuid4
 from sqlite3 import connect as sql_connect, Cursor, Row
 from http.server import HTTPServer, BaseHTTPRequestHandler
 from yt_dlp import YoutubeDL  # type: ignore
 import googleapiclient.discovery  # type: ignore
 
-# what we might want to manually define per environs
-API_KEY = environ.get('GOOGLE_API_KEY')
-HTTP_PORT = int(environ.get('YTPLOM_PORT', 8084))
+# default configuration
+DEFAULTS = {
+    'host': '127.0.0.1',  # NB: to be found remotely, use '0.0.0.0'!
+    'port': 8090,
+    'port_remote': 8090
+}
 
 # type definitions for mypy
 DatetimeStr = NewType('DatetimeStr', str)
 PATH_DB = PathStr(path_join(PATH_APP_DATA, 'db.sql'))
 PATH_TEMP = PathStr(path_join(PATH_CACHE, 'temp'))
 PATH_THUMBNAILS = PathStr(path_join(PATH_CACHE, 'thumbnails'))
+PATH_CONFFILE = PathStr(path_join(PATH_HOME, '.config/ytplom/config.json'))
 
 # template paths
 PATH_TEMPLATES = PathStr(path_join(PATH_APP_DATA, 'templates'))
 QUOTA_COST_YOUTUBE_SEARCH = QuotaCost(100)
 QUOTA_COST_YOUTUBE_DETAILS = QuotaCost(1)
 
-# local expectations
-TIMESTAMP_FMT = '%Y-%m-%d %H:%M:%S.%f'
-LEGAL_EXTENSIONS = {'webm', 'mp4', 'mkv'}
-
 # database stuff
 EXPECTED_DB_VERSION = 1
 SQL_DB_VERSION = SqlText('PRAGMA user_version')
                                    f'init_{EXPECTED_DB_VERSION}.sql'))
 
 # other
+ENVIRON_PREFIX = 'YTPLOM_'
+TIMESTAMP_FMT = '%Y-%m-%d %H:%M:%S.%f'
+LEGAL_EXTENSIONS = {'webm', 'mp4', 'mkv'}
 NAME_INSTALLER = PathStr('install.sh')
 VIDEO_FLAGS: dict[FlagName, FlagsInt] = {
   FlagName('delete'): FlagsInt(1 << 62)
         return list(conn.execute(SQL_DB_VERSION))[0][0]
 
 
+class Config:
+    """Collects user-configurable settings."""
+    host: str
+    remote: str
+    port: int
+    port_remote: int
+    api_key: str
+
+    def __init__(self):
+        def set_attrs_from_dict(d):
+            for attr_name, type_ in self.__class__.__annotations__.items():
+                if attr_name in d:
+                    setattr(self, attr_name, type_(d[attr_name]))
+        set_attrs_from_dict(DEFAULTS)
+        if isfile(PATH_CONFFILE):
+            with open(PATH_CONFFILE, 'r', encoding='utf8') as f:
+                conffile = json_load(f)
+            set_attrs_from_dict(conffile)
+        set_attrs_from_dict({k[len(ENVIRON_PREFIX):].lower(): v
+                             for k, v in environ.items()
+                             if k.isupper() and k.startswith(ENVIRON_PREFIX)})
+
+
 class DatabaseConnection:
     """Wrapped sqlite3.Connection."""
 
 class Server(HTTPServer):
     """Extension of HTTPServer providing for Player and DownloadsManager."""
 
-    def __init__(self, *args, **kwargs) -> None:
+    def __init__(self, config: Config, *args, **kwargs) -> None:
         super().__init__(*args, **kwargs)
+        self.config = config
         self.jinja = JinjaEnv(loader=JinjaFSLoader(PATH_TEMPLATES))
         self.player = Player()
         self.downloads = DownloadsManager()
 
         def collect_results(query_txt: QueryText) -> list[YoutubeVideo]:
             _ensure_expected_dirs([PATH_THUMBNAILS])
-            youtube = googleapiclient.discovery.build('youtube', 'v3',
-                                                      developerKey=API_KEY)
+            youtube = googleapiclient.discovery.build(
+                    'youtube', 'v3', developerKey=self.server.config.api_key)
             QuotaLog.update(conn, QUOTA_COST_YOUTUBE_SEARCH)
             search_request = youtube.search().list(
                     q=query_txt,