diff --git a/easytier-web/frontend/src/components/ConfigGenerator.vue b/easytier-web/frontend/src/components/ConfigGenerator.vue
index 2b79884..e9a8f7c 100644
--- a/easytier-web/frontend/src/components/ConfigGenerator.vue
+++ b/easytier-web/frontend/src/components/ConfigGenerator.vue
@@ -1,16 +1,32 @@
diff --git a/easytier-web/frontend/src/modules/api-host.ts b/easytier-web/frontend/src/modules/api-host.ts
new file mode 100644
index 0000000..0b58d32
--- /dev/null
+++ b/easytier-web/frontend/src/modules/api-host.ts
@@ -0,0 +1,64 @@
+const defaultApiHost = 'https://config-server.easytier.cn';
+
+interface ApiHost {
+ value: string;
+ usedAt: number;
+}
+
+const isValidHttpUrl = (s: string): boolean => {
+ let url;
+
+ try {
+ url = new URL(s);
+ } catch (_) {
+ return false;
+ }
+
+ return url.protocol === "http:" || url.protocol === "https:";
+};
+
+const cleanAndLoadApiHosts = (): Array => {
+ const maxHosts = 10;
+ const apiHosts = localStorage.getItem('apiHosts');
+ if (apiHosts) {
+ const hosts: Array = JSON.parse(apiHosts);
+ // sort by usedAt
+ hosts.sort((a, b) => b.usedAt - a.usedAt);
+
+ // only keep the first 10
+ if (hosts.length > maxHosts) {
+ hosts.splice(maxHosts);
+ }
+
+ localStorage.setItem('apiHosts', JSON.stringify(hosts));
+ return hosts;
+ } else {
+ return [];
+ }
+};
+
+const saveApiHost = (host: string) => {
+ console.log('Save API Host:', host);
+ if (!isValidHttpUrl(host)) {
+ console.error('Invalid API Host:', host);
+ return;
+ }
+
+ let hosts = cleanAndLoadApiHosts();
+ const newHost: ApiHost = {value: host, usedAt: Date.now()};
+ hosts = hosts.filter((h) => h.value !== host);
+ hosts.push(newHost);
+ localStorage.setItem('apiHosts', JSON.stringify(hosts));
+};
+
+const getInitialApiHost = (): string => {
+ const hosts = cleanAndLoadApiHosts();
+ if (hosts.length > 0) {
+ return hosts[0].value;
+ } else {
+ saveApiHost(defaultApiHost)
+ return defaultApiHost;
+ }
+};
+
+export {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost}
\ No newline at end of file