diff --git a/.env.example b/.env.example
new file mode 100644
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+MEDIAWIKI_ENTRY_POINT="/srv/mediawiki/index.php"
+DB_HOST="localhost"
+DB_USER="root"
+DB_PASS=""
diff --git a/.gitignore b/.gitignore
new file mode 100644
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+# Composer
+/vendor/
+composer.lock
+
+# DotEnv
+.env
diff --git a/LocalSettings.php b/LocalSettings.php
new file mode 100644
--- /dev/null
+++ b/LocalSettings.php
@@ -0,0 +1,22 @@
+<?php
+
+$service->run();
+$serviceConfiguration = $service->getConfiguration();
+
+$localDatabases = $serviceConfiguration->getLocalDatabases();
+
+$wgConf->wikis = $localDatabases;
+$wgConf->localVHosts = [ 'localhost' ];
+$wgConf->settings = $serviceConfiguration->getSettings();
+$wgConf->suffixes = $localDatabases;
+$wgConf->siteParamsCallback = 'Nasqueron\SAAS\MediaWiki\Hooks::onSitePameters';
+
+$wgDBname = $serviceConfiguration->getSelectedDatabase();
+$wgDBserver = $_ENV['DB_HOST'];
+$wgDBuser = $_ENV['DB_USER'];
+$wgDBpassword = $_ENV['DB_PASS'];
+
+$wgConf->extractAllGlobals( $wgDBname );
+
+wfLoadExtensions($serviceConfiguration->getResources('Extension'));
+wfLoadSkins($serviceConfiguration->getResources('Skin'));
diff --git a/composer.json b/composer.json
new file mode 100644
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,28 @@
+{
+    "name": "nasqueron/saas-mediawiki",
+    "description": "SaaS configuration entry point for MediaWiki",
+    "keywords": [
+      "nasqueron",
+      "SAAS",
+      "mediawiki",
+      "farm"
+    ],
+    "type": "project",
+    "license": "BSD-2-Clause",
+    "authors": [
+        {
+            "name": "Sébastien Santoro",
+            "email": "dereckson@espace-win.org"
+        }
+    ],
+    "require": {
+        "vlucas/phpdotenv": "^2.4"
+    },
+    "autoload": {
+      "psr-4": {
+        "Nasqueron\\SAAS\\MediaWiki\\": "src/",
+        "Nasqueron\\SAAS\\MediaWiki\\Configuration\\": "config/",
+        "Nasqueron\\SAAS\\MediaWiki\\Tests\\": "tests/"
+      }
+    }
+}
diff --git a/config/CommonSettings.php b/config/CommonSettings.php
new file mode 100644
--- /dev/null
+++ b/config/CommonSettings.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki\Configuration;
+
+class CommonSettings {
+
+    public static function mapSettings (array &$settings) : void {
+        $settings += self::getMappedSettings($settings);
+    }
+
+    public static function getMappedSettings (array $settings) : array {
+        $mappedSettings = [];
+        $mappedSettings += self::getRightsSettings($settings['saasLicense']);
+        return $mappedSettings;
+    }
+
+    ///
+    /// Individual set of settings
+    ///
+
+    private static function getRightsSettings (array $licenses) : array {
+        $settings = [];
+        foreach ($licenses as $key => $license) {
+            switch ($license) {
+                case 'CC-BY 4.0':
+                    $settings['wgRightsUrl'][$key] = 'http://creativecommons.org/licenses/by/4.0/';
+                    $settings['wgRightsText'][$key] = 'Creative Commons Attribution 4.0 International License';
+                    $settings['wgRightsIcon'][$key] = 'https://i.creativecommons.org/l/by/4.0/88x31.png';
+                    break;
+
+                default:
+                    throw new ConfigurationException("License unknown: $license");
+            }
+        }
+        return $settings;
+    }
+}
diff --git a/config/Instances.php b/config/Instances.php
new file mode 100644
--- /dev/null
+++ b/config/Instances.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki\Configuration;
+
+use Nasqueron\SAAS\MediaWiki\InstancesRepository;
+
+class Instances extends InstancesRepository {
+
+    static public function getList () : array {
+        return [
+            // Format: => database name
+
+            "agora.nasqueron.org" => "nasqueron_wiki",
+            "arsmagica.espace-win.org" => "arsmagica",
+            "utopia.espace-win.org" => "utopia",
+            "www.wolfplex.be" => "wolfplexdb",
+        ];
+    }
+
+    static public function getAliases () : array {
+        return [
+            // Format: Database => [ hosts ]
+
+            "wolfplexdb" => [
+                "www.wolfplex.org",
+                "wiki.wolfplex.org",
+                "wiki.wolfplex.be",
+            ]
+        ];
+    }
+
+}
diff --git a/config/MappableSettings.php b/config/MappableSettings.php
new file mode 100644
--- /dev/null
+++ b/config/MappableSettings.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki\Configuration;
+
+///
+/// Temporary hack to get clean configuration.
+///
+/// Plan is to deploy the MediaWiki SaaS on a dedicated node.
+///
+/// Meanwhile, as we share the main Nasqueron MySQL database,
+/// with an history of databases going back to 2001, we need
+/// to map nicely named site key to reml databases.
+
+class MappableSettings {
+
+    static public function getSettings () : array {
+        $settings = [];
+        foreach (static::getMappedSettings() as $setting => $values) {
+            $settings[$setting] = self::mapDatabases($values);
+        }
+        return $settings;
+    }
+
+    static private function mapDatabases ($items) {
+        $setting = [];
+        foreach ($items as $key => $value) {
+            $mappedKey = self::mapDatabase($key);
+            $setting[$mappedKey] = $value;
+        }
+        return $setting;
+    }
+
+    static private function mapDatabase ($key) {
+        foreach (static::getDatabaseMap() as $canonical => $actual) {
+            if ($key === $canonical) {
+                return $actual;
+            }
+        }
+
+        return $key;
+    }
+
+}
diff --git a/config/Settings.php b/config/Settings.php
new file mode 100644
--- /dev/null
+++ b/config/Settings.php
@@ -0,0 +1,98 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki\Configuration;
+
+class Settings extends MappableSettings {
+
+    static public function getDatabaseMap () : array {
+        return [
+            'agora' => 'nasqueron_wiki',
+            'wolfplex' => 'wolfplexdb',
+        ];
+    }
+
+    static protected function getMappedSettings () : array {
+        return [
+            'wgDBprefix' => [
+                'default' => '',
+
+                // Legacy installations
+                'arsmagica' => 'arsm_',
+                'utopia' => 'wiki_',
+                'wolfplex' => 'mw_', // shared database
+            ],
+
+            'wgSitename' => [
+                'agora' => 'Nasqueron Agora',
+                'arsmagica' => 'Ars Magica',
+                'utopia' => 'Utopia',
+                'wolfplex' => 'Wolfplex',
+            ],
+
+            'wgLanguageCode' => [
+                'default' => 'en',
+                'arsmagica' => 'fr',
+                'utopia' => 'fr',
+            ],
+
+            'wgArticlePath' => [
+                'default' => '/wiki/$1',
+                'arsmagica' => '/$1',
+            ],
+
+            'wgUploadDirectory' => [
+                'agora' => '',
+                'arsmagica' => '',
+                'utopia' => '',
+                'wolfplex' => '',
+            ],
+
+            'wgLogo' => [
+                // Do we serve /w/images for each wiki?
+                'default' => '/w/images/b/bc/Wiki.png',
+            ],
+
+            'wgEnableUploads' => [
+                'default' => false,
+            ],
+
+            'wgNamespacesWithSubpages' => [
+                'wolfplex' => [
+                    NS_MAIN => true,
+                ],
+                'nasqueron' => [
+                    NS_MAIN => true,
+                ],
+            ],
+
+            'saasLicense' => [
+                'default' => 'CC-BY 4.0',
+            ],
+
+            'wgEnableCreativeCommonsRdf' => [
+                'default' => true,
+            ],
+
+            'wgEnableDublinCoreRdf' => [
+                'default' => true,
+            ],
+
+            'saasUseExtensionCite' => [
+                'default' => true,
+            ],
+
+            'saasUseSkinMonoBook' => [
+                'default' => true,
+            ],
+
+            'saasUseSkinVector' => [
+                'default' => true,
+            ],
+
+            'saasUseSkinTimeless' => [
+                'default' => true,
+            ],
+        ];
+    }
+
+}
diff --git a/index.php b/index.php
new file mode 100644
--- /dev/null
+++ b/index.php
@@ -0,0 +1,14 @@
+<?php
+
+use Nasqueron\SAAS\MediaWiki\Service;
+
+// Composer PSR-4 autoloading and .env → environment
+require __DIR__ . '/vendor/autoload.php';
+(new Dotenv\Dotenv(__DIR__))->load();
+
+$service = new Service();
+$service
+    ->handleNotExistingSite()
+    ->handleAliveRequest();
+
+require $service->getEntryPoint();
diff --git a/src/Configuration.php b/src/Configuration.php
new file mode 100644
--- /dev/null
+++ b/src/Configuration.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki;
+
+use Nasqueron\SAAS\MediaWiki\Configuration\CommonSettings;
+use Nasqueron\SAAS\MediaWiki\Configuration\Instances;
+use Nasqueron\SAAS\MediaWiki\Configuration\Settings;
+
+class Configuration {
+
+    /**
+     * @var string
+     */
+    private $host;
+
+    public function __construct (string $host) {
+        $this->host = $host;
+    }
+
+    public function getLocalDatabases () : array {
+        return array_values(Instances::getList());
+    }
+
+    public function getSettings () : array {
+        // wg… keys
+        $settings = Settings::getSettings();
+
+        // saas… → wg… keys
+        CommonSettings::mapSettings($settings);
+
+        return $settings;
+    }
+
+    public function getResources (string $type) : array {
+        // saasUse<type><resource name>
+        // e.g. saasUseExtensionCite or saasUseSkinTimeless
+
+        $resources = [];
+
+        $prefix = "saasUse" . $type;
+        $len = strlen($prefix);
+        foreach ($GLOBALS as $key => $value) {
+            if (substr($key, 0, $len) === $prefix) {
+                $resources[] = substr($key, $len);
+            }
+        }
+
+        return $resources;
+    }
+
+    public function getSelectedDatabase () {
+        return Instances::getDatabaseFromHost($this->host);
+    }
+
+    ///
+    /// Helper methods
+    ///
+
+    public static function isSelectedWiki (string $wiki, string $suffix) : bool {
+        return substr($wiki, -strlen($suffix)) == $suffix;
+    }
+}
diff --git a/src/Hooks.php b/src/Hooks.php
new file mode 100644
--- /dev/null
+++ b/src/Hooks.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki;
+
+class Hooks {
+
+    public static function onSiteParameters ($conf, $wiki) {
+        $site = null;
+        $lang = null;
+
+        foreach ($conf->suffixes as $suffix) {
+            if (Configuration::isSelectedWiki($wiki, $suffix)) {
+                $site = $suffix;
+                $lang = substr( $wiki, 0, -strlen( $suffix ) );
+                break;
+            }
+        }
+
+        return [
+            'suffix' => $site,
+            'lang' => $lang,
+            'params' => [
+                'lang' => $lang,
+                'site' => $site,
+                'wiki' => $wiki,
+            ],
+            'tags' => [],
+        ];
+    }
+
+}
diff --git a/src/InstancesRepository.php b/src/InstancesRepository.php
new file mode 100644
--- /dev/null
+++ b/src/InstancesRepository.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki;
+
+abstract class InstancesRepository {
+
+    ///
+    /// Repository data methods
+    ///
+
+    abstract static public function getList () : array;
+    abstract static public function getAliases () : array;
+
+    ///
+    /// Helper methods
+    ///
+
+    public static function getDatabaseFromHost (string $host) {
+        // Case 1 - the host is canonical
+        $canonicalList = static::getList();
+        if (isset($canonicalList[$host])) {
+            return $canonicalList[$host];
+        }
+
+        // Case 2 - the host is an alias
+        foreach (static::getAliases() as $database => $vhost) {
+            if ($host === $vhost) {
+                return $database;
+            }
+        }
+
+        throw new InstanceNotFoundException($host);
+    }
+
+}
diff --git a/src/Service.php b/src/Service.php
new file mode 100644
--- /dev/null
+++ b/src/Service.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Nasqueron\SAAS\MediaWiki;
+
+class Service extends BaseService {
+
+    /**
+     * @var string
+     */
+    private $host;
+
+    /**
+     * @var Configuration
+     */
+    private $configuration = null;
+
+    public function __construct (string $host = '') {
+        $this->host = $host ?: self::getServerHost();
+    }
+
+    public function getHost () : string {
+        return $this->host;
+    }
+
+    public function run () : void {
+        $this->decorateHeaders();
+    }
+
+    private function decorateHeaders () : void {
+        header("SaaS-Host: " . $this->host);
+        header("SaaS-App: MediaWiki");
+    }
+
+    public function serveNotExistingResponse() : void {
+        header("HTTP/1.0 404 Not Found");
+        require 'views/404.php';
+        die;
+    }
+
+    public function getEntryPoint() : string {
+        return $_ENV['MEDIAWIKI_ENTRY_POINT'];
+    }
+
+    public function getConfiguration() : Configuration {
+        if ($this->configuration === null) {
+            $this->configuration = new Configuration($this->host);
+        }
+
+        return $this->configuration;
+    }
+
+}
+
+abstract class BaseService {
+
+    ///
+    /// Request methods
+    ///
+
+    public function handleNotExistingSite() : BaseService {
+        if (!$this->isExisting()) {
+            $this->serveNotExistingResponse();
+        }
+
+        return $this;
+    }
+
+    public function handleAliveRequest() : BaseService {
+        // Handles /status requests
+        if ($this->isAliveRequest()) {
+            $this->serveAliveResponse();
+        }
+
+        return $this;
+    }
+
+    public abstract function run();
+
+    ///
+    /// Default implementation
+    ///
+
+    public function isExisting () : bool {
+        return true;
+    }
+
+    public function serveAliveResponse() : void {
+        die("ALIVE");
+    }
+
+    public function serveNotExistingResponse(): void {
+        header("HTTP/1.0 404 Not Found");
+        die("This site doesn't exist.");
+    }
+
+    ///
+    /// Helper methods
+    ///
+
+    public function getServerHost() : string {
+        return self::extractHost($_SERVER['HTTP_HOST']);
+    }
+
+    /**
+     * Extracts hosts from a host:port expression
+     */
+    private static function extractHost ($host) : string {
+        $pos = strpos($host, ':');
+
+        if ($pos === false) {
+            return $host;
+        }
+
+        return substr ($host, 0, $pos);
+    }
+
+    public function getUri() : string {
+        $sources = [
+            'DOCUMENT_URI',
+            'REQUEST_URI',
+        ];
+
+        foreach ($sources as $source) {
+            if (isset($_SERVER[$source])) {
+                return $_SERVER[$source];
+            }
+        }
+
+        throw new \Exception("Can't get URI.");
+    }
+
+    private function isAliveRequest() : bool {
+        return
+            $_SERVER['REQUEST_METHOD'] === 'GET'
+        &&
+            $this->getUri() === '/status';
+    }
+
+}