diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css
index f8c266539..055572ed9 100644
--- a/web/source/css/_colors.css
+++ b/web/source/css/_colors.css
@@ -44,6 +44,9 @@ $blue1: #3a9fde; /* darker blue for smaller elements (borders), can only be used
 $blue2: #66befe; /* all-round accent color, can be used with $gray1 (6.8), $gray2 (5.5), $gray3 (4.9), $gray4 (4.5) */
 $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7.9), $gray2 (6.3), $gray3 (5.6), $gray4 (5.2), $gray5 (4.7) */
 
+$error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */
+$error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */
+
 $fg: $white1;
 $bg: $gray1;
 
diff --git a/web/source/package.json b/web/source/package.json
index 41c7260e7..51a49a478 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -38,7 +38,9 @@
     "react-error-boundary": "^3.1.4",
     "react-redux": "^8.0.2",
     "reactify": "^1.1.1",
+    "redux-devtools-extension": "^2.13.9",
     "redux-persist": "^6.0.0",
+    "redux-thunk": "^2.4.1",
     "uglifyify": "^5.0.2",
     "wouter": "^2.8.0-alpha.2"
   },
diff --git a/web/source/settings-panel/components/login.jsx b/web/source/settings-panel/components/login.jsx
index 180ab738f..636a7f326 100644
--- a/web/source/settings-panel/components/login.jsx
+++ b/web/source/settings-panel/components/login.jsx
@@ -18,6 +18,76 @@
 	
 "use strict";
 
+const Promise = require("bluebird");
+const React = require("react");
+const Redux = require("react-redux");
+
+const { setInstance } = require("../redux/reducers/instances").actions;
+const { updateInstance, updateRegistration } = require("../lib/api");
+
 module.exports = function Login() {
-	return (null);
+	const dispatch = Redux.useDispatch();
+	const [ instanceField, setInstanceField ] = React.useState("");
+	const [ errorMsg, setErrorMsg ] = React.useState();
+	const instanceFieldRef = React.useRef("");
+
+	React.useEffect(() => {
+		// check if current domain runs an instance
+		Promise.try(() => {
+			console.log("trying", window.location.origin);
+			return dispatch(updateInstance(window.location.origin));
+		}).then((json) => {
+			if (instanceFieldRef.current.length == 0) { // user hasn't started typing yet
+				dispatch(setInstance(json.uri));
+				instanceFieldRef.current = json.uri;
+				setInstanceField(json.uri);
+			}
+		}).catch((e) => {
+			console.log("Current domain does not host a valid instance: ", e);
+		});
+	}, []);
+
+	function tryInstance() {
+		Promise.try(() => {
+			return dispatch(updateInstance(instanceFieldRef.current)).catch((e) => {
+				// TODO: clearer error messages for common errors
+				console.log(e);
+				throw e;
+			});
+		}).then((instance) => {
+			// return dispatch(updateRegistration);
+		}).catch((e) => {
+			setErrorMsg(
+				<>
+					<b>{e.type}</b>
+					<span>{e.message}</span>
+				</>
+			);
+		});
+	}
+
+	function updateInstanceField(e) {
+		if (e.key == "Enter") {
+			tryInstance(instanceField);
+		} else {
+			setInstanceField(e.target.value);
+			instanceFieldRef.current = e.target.value;
+		}
+	}
+
+	return (
+		<section className="login">
+			<h1>OAUTH Login:</h1>
+			<form onSubmit={(e) => e.preventDefault()}>
+				<label htmlFor="instance">Instance: </label>
+				<input value={instanceField} onChange={updateInstanceField} id="instance"/>
+				{errorMsg && 
+				<div className="error">
+					{errorMsg}
+				</div>
+				}
+				<button onClick={tryInstance}>Authenticate</button>
+			</form>
+		</section>
+	);
 };
\ No newline at end of file
diff --git a/web/source/settings-panel/index.js b/web/source/settings-panel/index.js
index 01b8eeb03..bca2ead4e 100644
--- a/web/source/settings-panel/index.js
+++ b/web/source/settings-panel/index.js
@@ -33,7 +33,7 @@ const ErrorFallback = require("./components/error");
 
 const oauthLib = require("./lib/oauth");
 
-// require("./style.css");
+require("./style.css");
 
 // TODO: nested categories?
 const nav = {
@@ -90,7 +90,7 @@ function App() {
 					{sidebar}
 					{/* <button className="logout" onClick={oauth.logout}>Log out</button> */}
 				</div>
-				<section>
+				<section className="with-sidebar">
 					<Switch>
 						{panelRouter}
 					</Switch>
@@ -98,7 +98,9 @@ function App() {
 			</>
 		);
 	} else {
-		return <Login />;
+		return (
+			<Login />
+		);
 	}
 }
 
diff --git a/web/source/settings-panel/lib/api.js b/web/source/settings-panel/lib/api.js
new file mode 100644
index 000000000..e330ed64b
--- /dev/null
+++ b/web/source/settings-panel/lib/api.js
@@ -0,0 +1,94 @@
+/*
+	GoToSocial
+	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU Affero General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU Affero General Public License for more details.
+
+	You should have received a copy of the GNU Affero General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const { setRegistration } = require("../redux/reducers/oauth").actions;
+const { setInstanceInfo } = require("../redux/reducers/instances").actions;
+
+function apiCall(base, method, route, {payload, headers={}}) {
+	return Promise.try(() => {
+		let url = new URL(base);
+		url.pathname = route;
+		let body;
+
+		if (payload != undefined) {
+			body = JSON.stringify(payload);
+		}
+
+		let fetchHeaders = {
+			"Content-Type": "application/json",
+			...headers
+		};
+
+		return fetch(url.toString(), {
+			method: method,
+			headers: fetchHeaders,
+			body: body
+		});
+	}).then((res) => {
+		if (res.status == 200) {
+			return res.json();
+		} else {
+			throw res;
+		}
+	});
+}
+
+function getCurrentUrl() {
+	return `${window.location.origin}${window.location.pathname}`;
+}
+
+function updateInstance(domain) {
+	return function(dispatch, getState) {
+		/* check if domain is valid instance, then register client if needed  */
+
+		return Promise.try(() => {
+			return apiCall(domain, "GET", "/api/v1/instance", {
+				headers: {
+					"Content-Type": "text/plain"
+				}
+			});
+		}).then((json) => {
+			if (json && json.uri) { // TODO: validate instance json more?
+				dispatch(setInstanceInfo(json.uri, json));
+				return json;
+			}
+		});
+	};
+}
+
+function updateRegistration() {
+	return function(dispatch, getState) {
+		let base = getState().oauth.instance;
+		return Promise.try(() => {
+			return apiCall(base, "POST", "/api/v1/apps", {
+				client_name: "GoToSocial Settings",
+				scopes: "write admin",
+				redirect_uris: getCurrentUrl(),
+				website: getCurrentUrl()
+			});
+		}).then((json) => {
+			console.log(json);
+			dispatch(setRegistration(base, json));
+		});
+	};
+}
+
+module.exports = { updateInstance, updateRegistration };
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/index.js b/web/source/settings-panel/redux/index.js
index 12ad9d9b8..422bfab11 100644
--- a/web/source/settings-panel/redux/index.js
+++ b/web/source/settings-panel/redux/index.js
@@ -18,22 +18,27 @@
 
 "use strict";
 
-const { createStore, combineReducers } = require("redux");
+const { createStore, combineReducers, applyMiddleware } = require("redux");
 const { persistStore, persistReducer } = require("redux-persist");
+const thunk = require("redux-thunk").default;
+const { composeWithDevTools } = require("redux-devtools-extension");
 
 const persistConfig = {
 	key: "gotosocial-settings",
 	storage: require("redux-persist/lib/storage").default,
-	stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default
+	stateReconciler: require("redux-persist/lib/stateReconciler/autoMergeLevel2").default,
+	whitelist: ['oauth']
 };
 
 const combinedReducers = combineReducers({
-	oauth: require("./reducers/oauth").reducer
+	oauth: require("./reducers/oauth").reducer,
+	instances: require("./reducers/instances").reducer,
 });
 
 const persistedReducer = persistReducer(persistConfig, combinedReducers);
+const composedEnhancer = composeWithDevTools(applyMiddleware(thunk));
 
-const store = createStore(persistedReducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
+const store = createStore(persistedReducer, composedEnhancer);
 const persistor = persistStore(store);
 
 module.exports = { store, persistor };
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/instances.js b/web/source/settings-panel/redux/reducers/instances.js
new file mode 100644
index 000000000..92c7e982c
--- /dev/null
+++ b/web/source/settings-panel/redux/reducers/instances.js
@@ -0,0 +1,38 @@
+/*
+	GoToSocial
+	Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU Affero General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU Affero General Public License for more details.
+
+	You should have received a copy of the GNU Affero General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+"use strict";
+
+const {createSlice} = require("@reduxjs/toolkit");
+
+module.exports = createSlice({
+	name: "instances",
+	initialState: {
+		info: {},
+		current: undefined
+	},
+	reducers: {
+		setInstance: (state, {payload}) => {
+			state.current = payload;
+		},
+		setInstanceInfo: (state, {payload}) => {
+			let [key, info] = payload;
+			state.info[key] = info;
+		},
+	}
+});
\ No newline at end of file
diff --git a/web/source/settings-panel/redux/reducers/oauth.js b/web/source/settings-panel/redux/reducers/oauth.js
index d2186392c..a3202703e 100644
--- a/web/source/settings-panel/redux/reducers/oauth.js
+++ b/web/source/settings-panel/redux/reducers/oauth.js
@@ -23,20 +23,13 @@ const {createSlice} = require("@reduxjs/toolkit");
 module.exports = createSlice({
 	name: "oauth",
 	initialState: {
-		loggedIn: false
+		loggedIn: false,
+		registrations: {}
 	},
 	reducers: {
-		setInstance: (state, {payload}) => {
-			return {
-				...state,
-				instance: payload
-			};
-		},
 		setRegistration: (state, {payload}) => {
-			return {
-				...state,
-
-			}
+			let [key, info] = payload;
+			state.instanceRegistration[key] = info;
 		}
 	}
 });
\ No newline at end of file
diff --git a/web/source/settings-panel/style.css b/web/source/settings-panel/style.css
index 051038c58..a44e5d547 100644
--- a/web/source/settings-panel/style.css
+++ b/web/source/settings-panel/style.css
@@ -32,7 +32,7 @@ section {
 	display: grid;
 	grid-template-columns: 1fr min(92%, 90ch) 1fr;
 
-	section {
+	section.with-sidebar {
 		border-left: none;
 		border-top-left-radius: 0;
 		border-bottom-left-radius: 0;
@@ -128,10 +128,16 @@ input, select, textarea {
 }
 
 .error {
+	background: $error2;
+	border: 1px solid $error1;
+	border-radius: $br;
+	color: $error1;
 	font-weight: bold;
+	padding: 0.5rem;
 
 	pre {
 		background: $bg;
+		color: $fg;
 		padding: 1rem;
 		overflow: auto;
 		margin: 0;
diff --git a/web/source/yarn.lock b/web/source/yarn.lock
index 4f3cfd647..dd8056b87 100644
--- a/web/source/yarn.lock
+++ b/web/source/yarn.lock
@@ -4854,6 +4854,11 @@ recast@^0.11.17:
     private "~0.1.5"
     source-map "~0.5.0"
 
+redux-devtools-extension@^2.13.9:
+  version "2.13.9"
+  resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz#6b764e8028b507adcb75a1cae790f71e6be08ae7"
+  integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==
+
 redux-persist@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"