diff --git a/poetry.lock b/poetry.lock
index 68eefdb..03edad9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -210,6 +210,41 @@ mccabe = ">=0.6.0,<0.7.0"
 pycodestyle = ">=2.7.0,<2.8.0"
 pyflakes = ">=2.3.0,<2.4.0"
 
+[[package]]
+name = "gql"
+version = "2.0.0"
+description = "GraphQL client for Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+graphql-core = ">=2.3.2,<3"
+promise = ">=2.3,<3"
+requests = ">=2.12,<3"
+six = ">=1.10.0"
+
+[package.extras]
+dev = ["flake8 (==3.8.1)", "isort (==4.3.21)", "black (==19.10b0)", "mypy (==0.770)", "check-manifest (>=0.42,<1)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "coveralls (==2.0.0)"]
+test = ["pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "coveralls (==2.0.0)"]
+
+[[package]]
+name = "graphql-core"
+version = "2.3.2"
+description = "GraphQL implementation for Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+promise = ">=2.3,<3"
+rx = ">=1.6,<2"
+six = ">=1.10.0"
+
+[package.extras]
+gevent = ["gevent (>=1.1)"]
+test = ["six (==1.14.0)", "pyannotate (==1.2.0)", "pytest (==4.6.10)", "pytest-django (==3.9.0)", "pytest-cov (==2.8.1)", "coveralls (==1.11.1)", "cython (==0.29.17)", "gevent (==1.5.0)", "pytest-benchmark (==3.2.3)", "pytest-mock (==2.0.0)"]
+
 [[package]]
 name = "hyperlink"
 version = "21.0.0"
@@ -277,6 +312,20 @@ rsa = ["cryptography (>=3.0.0,<4)"]
 signals = ["blinker (>=1.4.0)"]
 signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
 
+[[package]]
+name = "promise"
+version = "2.3"
+description = "Promises/A+ implementation for Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+six = "*"
+
+[package.extras]
+test = ["pytest (>=2.7.3)", "pytest-cov", "coveralls", "futures", "pytest-benchmark", "mock"]
+
 [[package]]
 name = "pyasn1"
 version = "0.4.8"
@@ -411,6 +460,14 @@ requests = ">=2.0.0"
 [package.extras]
 rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
 
+[[package]]
+name = "rx"
+version = "1.6.1"
+description = "Reactive Extensions (Rx) for Python"
+category = "main"
+optional = false
+python-versions = "*"
+
 [[package]]
 name = "service-identity"
 version = "21.1.0"
@@ -596,7 +653,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "8e1e4f30b4d5f3cba3dc4b594a872f149b9aa3eeb6d3b5b721f5847c6aa2fd84"
+content-hash = "7ddd4096097eb58dc20601d3334a56507c893607ff156155070c4c865108c563"
 
 [metadata.files]
 asgiref = [
@@ -715,6 +772,14 @@ flake8 = [
     {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
     {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
 ]
+gql = [
+    {file = "gql-2.0.0-py2.py3-none-any.whl", hash = "sha256:35032ddd4bfe6b8f3169f806b022168932385d751eacc5c5f7122e0b3f4d6b88"},
+    {file = "gql-2.0.0.tar.gz", hash = "sha256:fe8d3a08047f77362ddfcfddba7cae377da2dd66f5e61c59820419c9283d4fb5"},
+]
+graphql-core = [
+    {file = "graphql-core-2.3.2.tar.gz", hash = "sha256:aac46a9ac524c9855910c14c48fc5d60474def7f99fd10245e76608eba7af746"},
+    {file = "graphql_core-2.3.2-py2.py3-none-any.whl", hash = "sha256:44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad"},
+]
 hyperlink = [
     {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"},
     {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"},
@@ -739,6 +804,9 @@ oauthlib = [
     {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
     {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
 ]
+promise = [
+    {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"},
+]
 pyasn1 = [
     {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
     {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
@@ -810,6 +878,10 @@ requests-oauthlib = [
     {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
     {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
 ]
+rx = [
+    {file = "Rx-1.6.1-py2.py3-none-any.whl", hash = "sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105"},
+    {file = "Rx-1.6.1.tar.gz", hash = "sha256:13a1d8d9e252625c173dc795471e614eadfe1cf40ffc684e08b8fff0d9748c23"},
+]
 service-identity = [
     {file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"},
     {file = "service_identity-21.1.0-py2.py3-none-any.whl", hash = "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db"},
diff --git a/pyproject.toml b/pyproject.toml
index c10021d..16a9ef7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,6 +14,7 @@ djangorestframework-dataclasses = "^0.9"
 social-auth-app-django = "^4.0.0"
 python-dotenv = "^0.17.1"
 websockets = "^9.1"
+gql = "^2.0.0"
 
 [tool.poetry.dev-dependencies]
 flake8 = "^3.9.2"
diff --git a/src/api.ts b/src/api.ts
new file mode 100644
index 0000000..c105f27
--- /dev/null
+++ b/src/api.ts
@@ -0,0 +1,19 @@
+export interface User {
+    active_config: number
+    active_version: string
+    custom_connection_gateway: string|null
+    custom_connection_gateway_token: string|null
+    is_pro: boolean
+}
+
+export interface Config {
+    id: number
+    content: string
+    last_used_with_version: string
+    created_at: Date
+    modified_at: Date
+}
+
+export interface Version {
+    version: string
+}
diff --git a/src/app.module.ts b/src/app.module.ts
index 2abb4c7..4ae70e4 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -1,24 +1,33 @@
-import { NgModule } from '@angular/core';
-import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
-import { BrowserModule } from '@angular/platform-browser';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { NgModule } from '@angular/core'
+import { NgbDropdownModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
+import { BrowserModule } from '@angular/platform-browser'
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
+import { CommonModule } from '@angular/common'
+import { FormsModule } from '@angular/forms'
 import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http'
 import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'
 import { AppComponent } from './components/app.component'
-import { MainComponent } from './components/main.component';
+import { MainComponent } from './components/main.component'
+import { ConfigModalComponent } from './components/configModal.component'
+import { SettingsModalComponent } from './components/settingsModal.component'
 
 @NgModule({
     imports: [
         BrowserModule,
         BrowserAnimationsModule,
+        CommonModule,
+        FormsModule,
         HttpClientModule,
         HttpClientXsrfModule,
         NgbDropdownModule,
+        NgbModalModule,
         FontAwesomeModule,
     ],
     declarations: [
         AppComponent,
         MainComponent,
+        ConfigModalComponent,
+        SettingsModalComponent,
     ],
     bootstrap: [AppComponent]
 })
diff --git a/src/components/app.component.ts b/src/components/app.component.ts
index 264c2c7..dc7257c 100644
--- a/src/components/app.component.ts
+++ b/src/components/app.component.ts
@@ -21,7 +21,9 @@ export class AppComponent {
 
     constructor (
         private loginService: LoginService,
-    ) { }
+    ) {
+        this.providers = [this.providers[0]] // only keep GH for now
+    }
 
     async ngOnInit () {
         await this.loginService.ready$.toPromise()
diff --git a/src/components/configModal.component.pug b/src/components/configModal.component.pug
new file mode 100644
index 0000000..57889b8
--- /dev/null
+++ b/src/components/configModal.component.pug
@@ -0,0 +1,40 @@
+.modal-body
+    .header(*ngIf='configService.activeConfig')
+        .d-flex.align-items-center.py-2.px-4
+            .me-auto
+                label Active config
+                .title {{configService.activeConfig.modified_at}}
+
+            button.btn.btn-semi.me-2((click)='configService.duplicateConfig()')
+                fa-icon([icon]='_copyIcon', [fixedWidth]='true')
+
+            button.btn.btn-semi((click)='deleteConfig()')
+                fa-icon([icon]='_deleteIcon', [fixedWidth]='true')
+
+        .d-flex.align-items-center.py-2.px-4(*ngIf='configService.activeVersion')
+            .me-auto App version:
+            div(ngbDropdown)
+                button.btn.btn-semi(ngbDropdownToggle) {{configService.activeVersion.version}}
+                div(ngbDropdownMenu)
+                    a(
+                        *ngFor='let version of versions',
+                        ngbDropdownItem,
+                        [class.active]='version == configService.activeVersion',
+                        (click)='selectVersion(version)'
+                    ) {{version.version}}
+
+    div(*ngIf='configService.configs.length > 1')
+        .dropdown-header All configs
+
+        ng-container(*ngFor='let config of configService.configs')
+            a(
+                *ngIf='config !== configService.activeConfig',
+                ngbDropdownItem,
+                (click)='selectConfig(config)',
+                href='#'
+            ) Config modified at {{config.modified_at}}
+
+    .p-3
+        button.btn.btn-semi.w-100((click)='configService.createNewConfig()')
+            fa-icon([icon]='_addIcon', [fixedWidth]='true')
+            span New config
diff --git a/src/components/configModal.component.ts b/src/components/configModal.component.ts
new file mode 100644
index 0000000..072a41c
--- /dev/null
+++ b/src/components/configModal.component.ts
@@ -0,0 +1,29 @@
+import { Component } from '@angular/core'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { AppConnectorService } from '../services/appConnector.service'
+import { ConfigService } from '../services/config.service'
+import { faPlus } from '@fortawesome/free-solid-svg-icons'
+
+@Component({
+    selector: 'config-modal',
+    templateUrl: './configModal.component.pug',
+    // styleUrls: ['./settingsModal.component.scss'],
+})
+export class ConfigModalComponent {
+    _addIcon = faPlus
+
+    constructor (
+        private modalInstance: NgbActiveModal,
+        public appConnector: AppConnectorService,
+        public configService: ConfigService,
+    ) {
+    }
+
+    async ngOnInit () {
+    }
+
+    cancel () {
+        this.modalInstance.dismiss()
+    }
+
+}
diff --git a/src/components/main.component.pug b/src/components/main.component.pug
index bd4a88e..aac01a1 100644
--- a/src/components/main.component.pug
+++ b/src/components/main.component.pug
@@ -1,66 +1,14 @@
 .sidebar
     img.logo(src='{{_logo}}')
 
-    div(ngbDropdown, placement='bottom-right')
-        button.btn(ngbDropdownToggle)
-            fa-icon([icon]='_cogIcon', [fixedWidth]='true')
+    button.btn((click)='openConfig()')
+        fa-icon([icon]='_cogIcon', [fixedWidth]='true')
 
-        .config-menu(ngbDropdownMenu)
-            .header(*ngIf='getActiveConfig()')
-                .d-flex.align-items-center.py-2.px-4
-                    .me-auto
-                        label Active config
-                        .title {{getActiveConfig().modified_at}}
+    button.btn((click)='openSettings()')
+        fa-icon([icon]='_settingsIcon', [fixedWidth]='true')
 
-                    button.btn.btn-semi.me-2((click)='duplicateConfig()')
-                        fa-icon([icon]='_copyIcon', [fixedWidth]='true')
+    button.btn.mt-auto((click)='logout()')
+        fa-icon([icon]='_logoutIcon', [fixedWidth]='true')
 
-                    button.btn.btn-semi((click)='deleteConfig()')
-                        fa-icon([icon]='_deleteIcon', [fixedWidth]='true')
-
-                .d-flex.align-items-center.py-2.px-4(*ngIf='activeVersion')
-                    .me-auto App version:
-                    div(ngbDropdown)
-                        button.btn.btn-semi(ngbDropdownToggle) {{activeVersion.version}}
-                        div(ngbDropdownMenu)
-                            a(
-                                *ngFor='let version of versions',
-                                ngbDropdownItem,
-                                [class.active]='version == activeVersion',
-                                (click)='selectVersion(version)'
-                            ) {{version.version}}
-
-            div(*ngIf='configs.length > 1')
-                .dropdown-header All configs
-
-                ng-container(*ngFor='let config of configs')
-                    a(
-                        *ngIf='config !== getActiveConfig()',
-                        ngbDropdownItem,
-                        (click)='selectConfig(config)',
-                        href='#'
-                    ) Config modified at {{config.modified_at}}
-
-            .p-3
-                button.btn.btn-semi.w-100((click)='createNewConfig()')
-                    fa-icon([icon]='_addIcon', [fixedWidth]='true')
-                    span New config
-
-    div(ngbDropdown, placement='bottom-right')
-        button.btn(ngbDropdownToggle)
-            fa-icon([icon]='_connectionIcon', [fixedWidth]='true')
-
-        div(ngbDropdownMenu)
-            .form-check.form-switch
-                input.form-check-input(type='checkbox', ngModel='!!loginService.user.custom_connection_gateway')
-                label(class='form-check-label') Use custom connection gateway
-
-    div(ngbDropdown, placement='bottom-right')
-        button.btn(ngbDropdownToggle)
-            fa-icon([icon]='_userIcon', [fixedWidth]='true')
-
-        div(ngbDropdownMenu)
-            a(ngbDropdownItem, (click)='logout()', href='#') Logout
-
-.terminal([hidden]='!activeVersion')
+.terminal([hidden]='!showApp')
     iframe(#iframe)
diff --git a/src/components/main.component.ts b/src/components/main.component.ts
index 76300f6..95ca649 100644
--- a/src/components/main.component.ts
+++ b/src/components/main.component.ts
@@ -1,10 +1,15 @@
-import * as semverGT from 'semver/functions/gt'
 import { Component, ElementRef, ViewChild } from '@angular/core'
 import { HttpClient } from '@angular/common/http'
 import { AppConnectorService } from '../services/appConnector.service'
 
-import { faCog, faUser, faCopy, faTrash, faPlus, faPlug } from '@fortawesome/free-solid-svg-icons'
+import { faCog, faCopy, faTrash, faPlus, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
 import { LoginService } from '../services/login.service'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { SettingsModalComponent } from './settingsModal.component'
+import { ConfigModalComponent } from './configModal.component'
+import { ConfigService } from '../services/config.service'
+import { combineLatest } from 'rxjs'
+import { Config, Version } from '../api'
 
 @Component({
     selector: 'main',
@@ -14,21 +19,22 @@ import { LoginService } from '../services/login.service'
 export class MainComponent {
     _logo = require('../assets/logo.svg')
     _cogIcon = faCog
-    _userIcon = faUser
+    _settingsIcon = faCog
+    _logoutIcon = faSignOutAlt
     _copyIcon = faCopy
     _addIcon = faPlus
     _deleteIcon = faTrash
-    _connectionIcon = faPlug
 
-    configs: any[] = []
-    versions: any[] = []
-    activeVersion?: any
+    showApp = false
+
     @ViewChild('iframe') iframe: ElementRef
 
     constructor (
-        private appConnector: AppConnectorService,
+        public appConnector: AppConnectorService,
         private http: HttpClient,
         public loginService: LoginService,
+        private ngbModal: NgbModal,
+        private config: ConfigService,
     ) {
         window.addEventListener('message', event => {
             if (event.data === 'request-connector') {
@@ -39,68 +45,47 @@ export class MainComponent {
     }
 
     async ngAfterViewInit () {
-        this.configs = await this.http.get('/api/1/configs').toPromise()
-        this.versions = await this.http.get('/api/1/versions').toPromise()
-
-        this.versions.sort((a, b) => semverGT(a, b))
-
-        if (!this.configs.length) {
-            await this.createNewConfig()
-        }
-
-        this.selectConfig(this.configs[0])
-    }
-
-    async createNewConfig () {
-        this.configs.push(await this.http.post('/api/1/configs', {
-            content: '{}',
-            last_used_with_version: this.versions[0].version,
-        }).toPromise())
-    }
-
-    async duplicateConfig () {
-        const copy = {...this.appConnector.config, pk: undefined}
-        this.configs.push(await this.http.post('/api/1/configs', copy).toPromise())
+        combineLatest(
+            this.config.activeConfig$,
+            this.config.activeVersion$
+        ).subscribe(([config, version]) => {
+            if (config && version) {
+                this.reloadApp(config, version)
+            }
+        })
+        this.config
+        await this.config.ready$.toPromise()
+        await this.config.selectDefaultConfig()
     }
 
     unloadApp () {
-        delete this.activeVersion
+        this.showApp = false
         this.iframe.nativeElement.src = 'about:blank'
     }
 
-    loadApp (version) {
+    async loadApp (config, version) {
+        this.showApp = true
         this.iframe.nativeElement.src = '/terminal'
-        this.activeVersion = version
+        await this.http.patch(`/api/1/configs/${config.id}`, {
+            last_used_with_version: version.version,
+        }).toPromise()
     }
 
-    getActiveConfig () {
-        return this.appConnector.config
-    }
-
-    selectVersion (version: any) {
+    reloadApp (config: Config, version: Version) {
         // TODO check config incompatibility
         this.unloadApp()
         setTimeout(() => {
-            this.appConnector.version = version
-            this.loadApp(version)
+            this.appConnector.setState(config, version)
+            this.loadApp(config, version)
         })
     }
 
-    async selectConfig (config: any) {
-        let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version)
-        if (!matchingVersion) {
-            // TODO ask to upgrade
-            matchingVersion = this.versions[0]
-        }
+    async openConfig () {
+        await this.ngbModal.open(ConfigModalComponent).result
+    }
 
-        this.appConnector.config = config
-
-        const result = await this.http.patch(`/api/1/configs/${config.id}`, {
-            last_used_with_version: matchingVersion.version,
-        }).toPromise()
-        Object.assign(config, result)
-
-        this.selectVersion(matchingVersion)
+    async openSettings () {
+        await this.ngbModal.open(SettingsModalComponent).result
     }
 
     async logout () {
diff --git a/src/components/settingsModal.component.pug b/src/components/settingsModal.component.pug
new file mode 100644
index 0000000..0bc395b
--- /dev/null
+++ b/src/components/settingsModal.component.pug
@@ -0,0 +1,32 @@
+.modal-header
+    h5.modal-title Settings
+.modal-body
+    .mb-3
+        .form-check.form-switch
+            input.form-check-input(
+                type='checkbox',
+                [(ngModel)]='customGatewayEnabled'
+            )
+            label(class='form-check-label') Use custom connection gateway
+
+    .mb-3(*ngIf='customGatewayEnabled')
+        .form-floating
+            input.form-control(
+                type='text',
+                [(ngModel)]='user.custom_connection_gateway',
+                placeholder='wss://1.2.3.4'
+            )
+            label Gateway address
+
+    .mb-3(*ngIf='customGatewayEnabled')
+        .form-floating
+            input.form-control(
+                type='password',
+                [(ngModel)]='user.custom_connection_gateway_token',
+                placeholder='123'
+            )
+            label Gateway authentication token
+
+.modal-footer
+    button.btn.btn-primary((click)='apply()') Apply
+    button.btn.btn-secondary((click)='cancel()') Cancel
diff --git a/src/components/settingsModal.component.ts b/src/components/settingsModal.component.ts
new file mode 100644
index 0000000..0f4051d
--- /dev/null
+++ b/src/components/settingsModal.component.ts
@@ -0,0 +1,36 @@
+import { Component } from '@angular/core'
+import { LoginService } from '../services/login.service'
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { User } from '../api'
+
+@Component({
+    selector: 'settings-modal',
+    templateUrl: './settingsModal.component.pug',
+    // styleUrls: ['./settingsModal.component.scss'],
+})
+export class SettingsModalComponent {
+    user: User
+    customGatewayEnabled = false
+
+    constructor (
+        private modalInstance: NgbActiveModal,
+        private loginService: LoginService,
+    ) {
+        this.user = { ...loginService.user }
+        this.customGatewayEnabled = !!this.user.custom_connection_gateway
+    }
+
+    async ngOnInit () {
+    }
+
+    async apply () {
+        Object.assign(this.loginService.user, this.user)
+        this.modalInstance.close()
+        await this.loginService.updateUser()
+    }
+
+    cancel () {
+        this.modalInstance.dismiss()
+    }
+}
diff --git a/src/services/appConnector.service.ts b/src/services/appConnector.service.ts
index 69d7a02..9ec831c 100644
--- a/src/services/appConnector.service.ts
+++ b/src/services/appConnector.service.ts
@@ -4,6 +4,7 @@ import { debounceTime } from 'rxjs/operators'
 import { HttpClient } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { LoginService } from '../services/login.service'
+import { Config, Version } from '../api'
 
 export class SocketProxy {
     connect$ = new Subject<void>()
@@ -86,10 +87,9 @@ export class SocketProxy {
 
 @Injectable({ providedIn: 'root' })
 export class AppConnectorService {
-    config: any
-    version: any
-    user: any
     private configUpdate = new Subject<string>()
+    private config: Config
+    private version: Version
 
     constructor (
         private http: HttpClient,
@@ -101,6 +101,11 @@ export class AppConnectorService {
         })
     }
 
+    setState (config: Config, version: Version) {
+        this.config = config
+        this.version = version
+    }
+
     async loadConfig (): Promise<string> {
         return this.config.content
     }
diff --git a/src/services/config.service.ts b/src/services/config.service.ts
new file mode 100644
index 0000000..4f05b49
--- /dev/null
+++ b/src/services/config.service.ts
@@ -0,0 +1,83 @@
+import * as semverGT from 'semver/functions/gt'
+import { AsyncSubject, Subject } from 'rxjs'
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { Config, User, Version } from '../api'
+import { LoginService } from './login.service'
+
+
+@Injectable({ providedIn: 'root' })
+export class ConfigService {
+    activeConfig$ = new Subject<Config>()
+    activeVersion$ = new Subject<Version>()
+    user: User
+
+    configs: Config[] = []
+    versions: Version[] = []
+    ready$ = new AsyncSubject<void>()
+
+    get activeConfig (): Config { return this._activeConfig }
+    get activeVersion (): Version { return this._activeVersion }
+
+    private _activeConfig: Config|null = null
+    private _activeVersion: Version|null = null
+
+    constructor (
+        private http: HttpClient,
+        private loginService: LoginService,
+    ) {
+        this.init()
+    }
+
+    async updateUser () {
+        await this.http.put('/api/1/user', this.user).toPromise()
+    }
+
+    async createNewConfig () {
+        this.configs.push(await this.http.post('/api/1/configs', {
+            content: '{}',
+            last_used_with_version: this._activeVersion.version,
+        }).toPromise())
+    }
+
+    async duplicateActiveConfig () {
+        const copy = {...this._activeConfig, pk: undefined}
+        this.configs.push(await this.http.post('/api/1/configs', copy).toPromise())
+    }
+
+    async selectVersion (version: Version) {
+        this._activeVersion = version
+        this.activeVersion$.next(version)
+    }
+
+    async selectConfig (config: Config) {
+        let matchingVersion = this.versions.find(x => x.version === config.last_used_with_version)
+        if (!matchingVersion) {
+            // TODO ask to upgrade
+            matchingVersion = this.versions[0]
+        }
+
+        this._activeConfig = config
+        this.activeConfig$.next(config)
+        this.selectVersion(matchingVersion)
+    }
+
+    async selectDefaultConfig () {
+        await this.ready$.toPromise()
+        await this.loginService.ready$.toPromise()
+        this.selectConfig(this.configs.find(c => c.id === this.loginService.user.active_config) ?? this.configs[0])
+    }
+
+    private async init () {
+        this.configs = await this.http.get('/api/1/configs').toPromise()
+        this.versions = await this.http.get('/api/1/versions').toPromise()
+        this.versions.sort((a, b) => semverGT(a, b))
+
+        if (!this.configs.length) {
+            await this.createNewConfig()
+        }
+
+        this.ready$.next()
+        this.ready$.complete()
+    }
+}
diff --git a/src/services/login.service.ts b/src/services/login.service.ts
index 8507800..717d8f7 100644
--- a/src/services/login.service.ts
+++ b/src/services/login.service.ts
@@ -1,20 +1,29 @@
 import { AsyncSubject } from 'rxjs'
 import { HttpClient } from '@angular/common/http'
 import { Injectable } from '@angular/core'
+import { User } from '../api'
 
 
 @Injectable({ providedIn: 'root' })
 export class LoginService {
-    user: any
+    user: User
     ready$ = new AsyncSubject<void>()
 
     constructor (private http: HttpClient) {
         this.init()
     }
 
+    async updateUser () {
+        await this.http.put('/api/1/user', this.user).toPromise()
+    }
+
     private async init () {
-        const user = await this.http.get('/api/1/user').toPromise()
-        this.user = user
+        try {
+            this.user = await this.http.get('/api/1/user').toPromise()
+        } catch {
+            this.user = null
+        }
+
         this.ready$.next()
         this.ready$.complete()
     }
diff --git a/src/terminal.ts b/src/terminal.ts
index e4febff..3c24902 100644
--- a/src/terminal.ts
+++ b/src/terminal.ts
@@ -1,36 +1,6 @@
 import './terminal-styles.scss'
 
-
-Object.assign(window, {
-    // Buffer,
-    process: {
-        env: { },
-        argv: ['terminus'],
-        platform: 'darwin',
-        on: () => null,
-        stdout: {},
-        stderr: {},
-        resourcesPath: 'resources',
-        version: '14.0.0',
-        nextTick: (f, ...args) => setTimeout(() => f(...args)),
-        // cwd: () => '/',
-    },
-    global: window,
-})
-
-
 async function start () {
-    const modules = { }
-
-    Object.assign(window, {
-        require: (path) => {
-            if (modules[path]) {
-                return modules[path]
-            }
-            console.error('requiring real module', path)
-        },
-    })
-
     await new Promise<void>(resolve => {
         window.addEventListener('message', event => {
             if (event.data === 'connector-ready') {
@@ -40,10 +10,11 @@ async function start () {
         window.parent.postMessage('request-connector', '*')
     })
 
-    const appVersion = window['__connector__'].getAppVersion()
+    const connector = window['__connector__']
 
-    async function loadPlugin (name, file = 'index.js') {
-        const url = `../app-dist/${appVersion}/${name}/dist/${file}`
+    const appVersion = connector.getAppVersion()
+
+    async function webRequire (url) {
         console.log(`Loading ${url}`)
         const e = document.createElement('script')
         window['module'] = { exports: {} } as any
@@ -56,8 +27,11 @@ async function start () {
         return window['module'].exports
     }
 
-    await loadPlugin('web', 'preload.js')
-    await loadPlugin('web', 'bundle.js')
+    const baseUrl = `../app-dist/${appVersion}`
+    await webRequire(`${baseUrl}/web/dist/preload.js`)
+    await webRequire(`${baseUrl}/web/dist/bundle.js`)
+
+    const terminus = window['Terminus']
 
     const pluginModules = []
     for (const plugin of [
@@ -68,15 +42,13 @@ async function start () {
         'terminus-community-color-schemes',
         'terminus-web',
     ]) {
-        const mod = await loadPlugin(plugin)
-        modules[`resources/builtin-plugins/${plugin}`] = modules[plugin] = mod
-        pluginModules.push(mod)
+        pluginModules.push(await terminus.loadPlugin(`${baseUrl}/${plugin}`))
     }
 
     document.querySelector('app-root')['style'].display = 'flex'
 
-    const config = window['__connector__'].loadConfig()
-    window['bootstrapTerminus'](pluginModules, {
+    const config = connector.loadConfig()
+    terminus.bootstrap(pluginModules, {
         config,
         executable: 'web',
         isFirstWindow: true,
diff --git a/src/theme/index.scss b/src/theme/index.scss
index 94c4707..f6f9664 100644
--- a/src/theme/index.scss
+++ b/src/theme/index.scss
@@ -29,7 +29,7 @@
 @import "~bootstrap/scss/list-group";
 // @import "~bootstrap/scss/close";
 // @import "~bootstrap/scss/toasts";
-// @import "~bootstrap/scss/modal";
+@import "~bootstrap/scss/modal";
 // @import "~bootstrap/scss/tooltip";
 // @import "~bootstrap/scss/popover";
 // @import "~bootstrap/scss/carousel";
@@ -62,3 +62,7 @@
 .dropdown-menu {
   box-shadow: $dropdown-box-shadow;
 }
+
+.modal-footer {
+  background: #00000030;
+}
diff --git a/src/theme/vars.scss b/src/theme/vars.scss
index 013a08a..c306aba 100644
--- a/src/theme/vars.scss
+++ b/src/theme/vars.scss
@@ -77,7 +77,7 @@ $link-hover-color: $white;
 $link-hover-decoration: none;
 
 $component-active-color: $white;
-$component-active-bg: #2f3a42;
+$component-active-bg: $blue;
 
 $list-group-bg: $table-bg;
 $list-group-border-color: $table-border-color;
@@ -113,7 +113,7 @@ $popover-max-width: 360px;
 
 $btn-border-width:  2px;
 
-$input-bg:                       #181e23;
+$input-bg:                       $black;
 $input-disabled-bg:              #2e3235;
 
 $input-color:                    #ddd;
@@ -129,6 +129,8 @@ $input-group-addon-bg: $input-bg;
 $input-group-addon-border-color: transparent;
 $input-group-btn-border-color: $input-bg;
 
+$form-switch-color:               rgba(255,255,255, .25);
+
 $nav-tabs-border-radius: 0;
 $nav-tabs-border-color: transparent;
 $nav-tabs-border-width: 2px;
@@ -179,8 +181,7 @@ $custom-control-indicator-active-bg: rgba(255, 255, 0, 0.5);
 $modal-content-bg:               $body-bg;
 $modal-content-border-color:     $body-bg;
 $modal-header-border-width:         0;
-$modal-footer-border-color:         #222;
-$modal-footer-border-width:         1px;
+$modal-footer-border-width:         0;
 $modal-content-border-width:        0;
 
 $progress-bg: $table-bg;
diff --git a/terminus/app/api.py b/terminus/app/api.py
index 988826d..647d5ca 100644
--- a/terminus/app/api.py
+++ b/terminus/app/api.py
@@ -3,12 +3,13 @@ from dataclasses import dataclass
 from django.conf import settings
 from django.contrib.auth import logout
 from rest_framework import fields
+from rest_framework.exceptions import PermissionDenied
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
-from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
+from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
 from rest_framework.views import APIView
 from rest_framework.viewsets import GenericViewSet, ModelViewSet
-from rest_framework.serializers import ModelSerializer, Field
+from rest_framework.serializers import ModelSerializer
 from rest_framework_dataclasses.serializers import DataclassSerializer
 
 from .models import Config, User
@@ -64,21 +65,25 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet):
 
 class UserSerializer(ModelSerializer):
     id = fields.IntegerField()
+    is_pro = fields.SerializerMethodField()
 
     class Meta:
         model = User
-        fields = ('id', 'username', 'active_config', 'custom_connection_gateway', 'custom_connection_gateway_token')
+        fields = ('id', 'username', 'active_config', 'custom_connection_gateway', 'custom_connection_gateway_token', 'is_pro')
         read_only_fields = ('id', 'username')
 
+    def get_is_pro(self, obj):
+        return False
 
-class UserViewSet(RetrieveModelMixin, GenericViewSet):
+
+class UserViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
     queryset = User.objects.all()
     serializer_class = UserSerializer
 
     def get_object(self):
         if self.request.user.is_authenticated:
             return self.request.user
-        return None
+        raise PermissionDenied()
 
 
 class LogoutView(APIView):
diff --git a/terminus/app/sponsors.py b/terminus/app/sponsors.py
new file mode 100644
index 0000000..ebc5bdb
--- /dev/null
+++ b/terminus/app/sponsors.py
@@ -0,0 +1,63 @@
+from django.conf import settings
+from gql import Client, gql
+from gql.transport.requests import RequestsHTTPTransport
+
+
+GQL_ENDPOINT = 'https://api.github.com/graphql'
+
+
+def get_sponsor_usernames():
+    client = Client(
+        transport=RequestsHTTPTransport(
+            url=GQL_ENDPOINT,
+            use_json=True,
+            headers={
+                'Authorization': f'Bearer {settings.GITHUB_TOKEN}',
+            }
+        )
+    )
+
+    result = []
+
+    after = None
+
+    while True:
+        params = 'first: 1'
+        if after:
+            params += f', after:"{after}"'
+
+        query = '''
+            query {
+                user (login: "eugeny") {
+                    sponsorshipsAsMaintainer(%s, includePrivate: true) {
+                    pageInfo {
+                        startCursor
+                        hasNextPage
+                        endCursor
+                    }
+                    nodes {
+                        createdAt
+                            tier {
+                                monthlyPriceInDollars
+                            }
+                            sponsor{
+                                ... on User {
+                                    login
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        ''' % (params,)
+
+        response = client.execute(gql(query))
+        after = response['user']['sponsorshipsAsMaintainer']['pageInfo']['endCursor']
+        nodes = response['user']['sponsorshipsAsMaintainer']['nodes']
+        if not len(nodes):
+            break
+        for node in nodes:
+            if node['tier']['monthlyPriceInDollars'] >= settings.GITHUB_SPONSORS_MIN_PAYMENT:
+                result.append(node['sponsor']['login'])
+
+    return result
diff --git a/terminus/app/urls.py b/terminus/app/urls.py
index 40a6e3e..80adb38 100644
--- a/terminus/app/urls.py
+++ b/terminus/app/urls.py
@@ -12,7 +12,7 @@ router.register('api/1/versions', api.AppVersionViewSet, basename='app-versions'
 
 urlpatterns = [
     path('api/1/auth/logout', api.LogoutView.as_view()),
-    path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve'})),
+    path('api/1/user', api.UserViewSet.as_view({'get': 'retrieve', 'put': 'update'})),
 
     path('', views.IndexView.as_view()),
     path('terminal', views.TerminalView.as_view()),
diff --git a/terminus/settings.py b/terminus/settings.py
index 101a310..8ffc368 100644
--- a/terminus/settings.py
+++ b/terminus/settings.py
@@ -136,6 +136,8 @@ AUTHENTICATION_BACKENDS = (
     'django.contrib.auth.backends.ModelBackend',
 )
 
+SOCIAL_AUTH_GITHUB_SCOPE = ['read:user', 'user:email']
+
 LOGIN_REDIRECT_URL = '/'
 
 APP_DIST_PATH = BASE_DIR / 'app-dist'
@@ -152,9 +154,19 @@ for key in [
     'CONNECTION_GATEWAY_AUTH_CA',
     'CONNECTION_GATEWAY_AUTH_CERTIFICATE',
     'CONNECTION_GATEWAY_AUTH_KEY',
+    'GITHUB_SPONSORS_USER',
+    'GITHUB_SPONSORS_MIN_PAYMENT',
+    'GITHUB_TOKEN',
 ]:
     globals()[key] = os.getenv(key)
 
+
+for key in [
+    'GITHUB_SPONSORS_MIN_PAYMENT',
+]:
+    globals()[key] = int(globals()[key]) if globals()[key] else None
+
+
 for key in [
     'CONNECTION_GATEWAY_AUTH_CA',
     'CONNECTION_GATEWAY_AUTH_CERTIFICATE',
diff --git a/webpack.config.js b/webpack.config.js
index ca4fed1..45df1d9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,4 +1,69 @@
-module.exports = [
-  require('./webpack.main.config'),
-  require('./webpack.terminal.config'),
-]
+const path = require('path')
+const webpack = require('webpack')
+const { AngularWebpackPlugin } =  require('@ngtools/webpack')
+
+module.exports = {
+  target: 'web',
+  entry: {
+    'index.ignore': 'file-loader?name=index.html!pug-html-loader!' + path.resolve(__dirname, './src/index.pug'),
+    index: path.resolve(__dirname, 'src/index.ts'),
+    'terminal.ignore': 'file-loader?name=terminal.html!pug-html-loader!' + path.resolve(__dirname, './src/terminal.pug'),
+    terminal: path.resolve(__dirname, 'src/terminal.ts'),
+  },
+  mode: process.env.DEV ? 'development' : 'production',
+  context: __dirname,
+  devtool: 'cheap-module-source-map',
+  output: {
+    path: path.join(__dirname, 'build'),
+    pathinfo: true,
+    filename: '[name].js',
+    chunkFilename: '[name].bundle.js',
+  },
+  resolve: {
+    modules: [
+      'src/',
+      'node_modules/',
+    ],
+    extensions: ['.ts', '.js'],
+  },
+  module: {
+    rules: [
+      {
+        test: /\.[jt]sx?$/,
+        loader: '@ngtools/webpack',
+      },
+      { test: /terminus\/app\/dist/, use: ['script-loader'] },
+      {
+        test: /\.pug$/,
+        use: ['apply-loader', 'pug-loader'],
+        include: /component\.pug/
+      },
+      {
+        test: /\.scss$/,
+        use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'],
+        include: /component\.scss/
+      },
+      {
+        test: /\.scss$/,
+        use: ['style-loader', 'css-loader', 'sass-loader'],
+        exclude: /component\.scss/
+      },
+      {
+        test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
+        type: 'asset/resource',
+      },
+      { test: /\.css$/, use: ['css-loader', 'sass-loader'] },
+      {
+        test: /\.(jpeg|png|svg)?$/,
+        type: 'asset/resource',
+      }
+    ],
+  },
+
+  plugins: [
+    new AngularWebpackPlugin({
+      tsconfig: 'tsconfig.main.json',
+      directTemplateLoading: false,
+    }),
+  ],
+}
diff --git a/webpack.main.config.js b/webpack.main.config.js
deleted file mode 100644
index 227b09e..0000000
--- a/webpack.main.config.js
+++ /dev/null
@@ -1,67 +0,0 @@
-const path = require('path')
-const webpack = require('webpack')
-const { AngularWebpackPlugin } =  require('@ngtools/webpack')
-
-module.exports = {
-  target: 'web',
-  entry: {
-    'index.ignore': 'file-loader?name=index.html!pug-html-loader!' + path.resolve(__dirname, './src/index.pug'),
-    index: path.resolve(__dirname, 'src/index.ts'),
-  },
-  mode: process.env.DEV ? 'development' : 'production',
-  context: __dirname,
-  devtool: 'cheap-module-source-map',
-  output: {
-    path: path.join(__dirname, 'build'),
-    pathinfo: true,
-    filename: '[name].js',
-    chunkFilename: '[name].bundle.js',
-  },
-  resolve: {
-    modules: [
-      'src/',
-      'node_modules/',
-    ],
-    extensions: ['.ts', '.js'],
-  },
-  module: {
-    rules: [
-      {
-        test: /\.[jt]sx?$/,
-        loader: '@ngtools/webpack',
-      },
-      { test: /terminus\/app\/dist/, use: ['script-loader'] },
-      {
-        test: /\.pug$/,
-        use: ['apply-loader', 'pug-loader'],
-        include: /component\.pug/
-      },
-      {
-        test: /\.scss$/,
-        use: ['@terminus-term/to-string-loader', 'css-loader', 'sass-loader'],
-        include: /component\.scss/
-      },
-      {
-        test: /\.scss$/,
-        use: ['style-loader', 'css-loader', 'sass-loader'],
-        exclude: /component\.scss/
-      },
-      {
-        test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
-        type: 'asset/resource',
-      },
-      { test: /\.css$/, use: ['css-loader', 'sass-loader'] },
-      {
-        test: /\.(jpeg|png|svg)?$/,
-        type: 'asset/resource',
-      }
-    ],
-  },
-
-  plugins: [
-    new AngularWebpackPlugin({
-      tsconfig: 'tsconfig.main.json',
-      directTemplateLoading: false,
-    }),
-  ],
-}
diff --git a/webpack.terminal.config.js b/webpack.terminal.config.js
deleted file mode 100644
index 9bd6dbd..0000000
--- a/webpack.terminal.config.js
+++ /dev/null
@@ -1,72 +0,0 @@
-const path = require('path')
-const webpack = require('webpack')
-
-module.exports = {
-  name: 'web-container-terminal',
-  target: 'web',
-  entry: {
-    'terminal.ignore': 'file-loader?name=terminal.html!pug-html-loader!' + path.resolve(__dirname, './src/terminal.pug'),
-    terminal: path.resolve(__dirname, 'src/terminal.ts'),
-  },
-  mode: process.env.DEV ? 'development' : 'production',
-  context: __dirname,
-  devtool: 'cheap-module-source-map',
-  output: {
-    path: path.join(__dirname, 'build'),
-    pathinfo: true,
-    filename: '[name].js',
-    chunkFilename: '[name].bundle.js',
-  },
-  resolve: {
-    modules: [
-      ...[
-        // '../terminus/terminus-core/node_modules/',
-        // '../terminus/terminus-settings/node_modules/',
-        // '../terminus/terminus-terminal/node_modules/',
-        // '../terminus/node_modules',
-        // '../terminus/app/node_modules',
-        // '../terminus/app/assets/',
-        'src',
-      ].map(x => path.join(__dirname, x)),
-      'node_modules/',
-    ],
-    extensions: ['.ts', '.js'],
-  },
-  module: {
-    rules: [
-      {
-        test: /\.ts$/,
-        use: {
-          loader: 'awesome-typescript-loader',
-          options: {
-            configFileName: path.resolve(__dirname, 'tsconfig.container.json'),
-          },
-        },
-      },
-      //   { test: /terminus\/app\/dist/, use: ['script-loader'] },
-      {
-        test: /\.scss$/,
-        use: ['style-loader', 'css-loader', 'sass-loader'],
-      },
-      {
-        test: /\.(ttf|eot|otf|woff|woff2)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
-        use: {
-          loader: 'file-loader',
-          options: {
-            name: 'fonts/[name].[ext]',
-          },
-        },
-      },
-      { test: /\.css$/, use: ['css-loader', 'sass-loader'] },
-      {
-        test: /\.(jpeg|png|svg)?$/,
-        use: {
-          loader: 'file-loader',
-          options: {
-            name: '[name].[ext]'
-          }
-        }
-      }
-    ],
-  }
-}