diff --git a/packages/dbtype/src/api.ts b/packages/dbtype/src/api.ts
index 1634c7e..6ee2075 100644
--- a/packages/dbtype/src/api.ts
+++ b/packages/dbtype/src/api.ts
@@ -53,6 +53,12 @@ export const UserSchema = z.object({
 
 export type User = z.infer<typeof UserSchema>;
 
+export const UserSettingSchema = z.object({
+	fileDeepLinkRegex: z.string().optional(),
+});
+
+export type UserSetting = z.infer<typeof UserSettingSchema>;
+
 export const SchemaMigrationSchema = z.object({
 	version: z.string().nullable(),
 	dirty: z.boolean(),
diff --git a/packages/dbtype/src/types.ts b/packages/dbtype/src/types.ts
index cbdb075..d7bd3d2 100644
--- a/packages/dbtype/src/types.ts
+++ b/packages/dbtype/src/types.ts
@@ -45,6 +45,11 @@ export interface Users {
   username: string | null;
 }
 
+export interface UserSettings {
+  username: string;
+  settings: string | null;
+}
+
 export interface DB {
   doc_tag_relation: DocTagRelation;
   document: Document;
@@ -52,4 +57,5 @@ export interface DB {
   schema_migration: SchemaMigration;
   tags: Tags;
   users: Users;
+  user_settings: UserSettings;
 }
diff --git a/packages/server/migrations/2025-06-26.ts b/packages/server/migrations/2025-06-26.ts
index 08e8e77..43688e4 100644
--- a/packages/server/migrations/2025-06-26.ts
+++ b/packages/server/migrations/2025-06-26.ts
@@ -4,7 +4,7 @@ export async function up(db: Kysely<any>) {
     await db.schema
         .createTable("user_settings")
         .addColumn("username", "varchar(256)", col => col.notNull().primaryKey())
-        .addColumn("settings", "jsonb", col => col.notNull())
+        .addColumn("settings", "json", col => col.notNull())
         .addForeignKeyConstraint("user_settings_username_fk", ["username"], "users", ["username"])
         .execute();
 
diff --git a/packages/server/src/db/user.ts b/packages/server/src/db/user.ts
index 84cdf5f..d73cf2e 100644
--- a/packages/server/src/db/user.ts
+++ b/packages/server/src/db/user.ts
@@ -1,5 +1,5 @@
 import { getKysely } from "./kysely.ts";
-import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
+import { type IUser, IUserSettings, Password, type UserAccessor, type UserCreateInput } from "../model/user.ts";
 
 class SqliteUser implements IUser {
 	readonly username: string;
@@ -41,6 +41,24 @@ class SqliteUser implements IUser {
 			.executeTakeFirst();
 		return (result.numDeletedRows ?? 0n) > 0;
 	}
+	async get_settings(): Promise<IUserSettings | undefined> {
+		const settings = await this.kysely
+			.selectFrom("user_settings")
+			.select("settings")
+			.where("username", "=", this.username)
+			.executeTakeFirst();
+		if (!settings) return undefined;
+		return settings.settings ? JSON.parse(settings.settings) as IUserSettings : undefined;
+	}
+	async set_settings(settings: IUserSettings) {
+		const settingsJson = JSON.stringify(settings);
+		const result = await this.kysely
+			.insertInto("user_settings")
+			.values({ username: this.username, settings: settingsJson })
+			.onConflict((oc) => oc.doUpdateSet({ settings: settingsJson }))
+			.executeTakeFirst();
+		return (result.numInsertedOrUpdatedRows ?? 0n) > 0;
+	}
 }
 
 export const createSqliteUserController = (kysely = getKysely()): UserAccessor => {
diff --git a/packages/server/src/login.ts b/packages/server/src/login.ts
index 7d347c9..d38c9b3 100644
--- a/packages/server/src/login.ts
+++ b/packages/server/src/login.ts
@@ -76,7 +76,7 @@ function setToken(ctx: Koa.Context, token_name: string, token_payload: string |
 	});
 }
 
-export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
+export const createLoginHandler = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
 	const setting = get_setting();
 	const secretKey = setting.jwt_secretkey;
 	const body = ctx.request.body;
@@ -115,7 +115,7 @@ export const createLoginMiddleware = (userController: UserAccessor) => async (ct
 	return;
 };
 
-export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
+export const LogoutHandler = (ctx: Koa.Context, _next: Koa.Next) => {
 	const setting = get_setting();
 	ctx.cookies.set(accessTokenName, null);
 	ctx.cookies.set(refreshTokenName, null);
@@ -127,9 +127,9 @@ export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
 	return;
 };
 
-export const createUserMiddleWare =
+export const createUserHandler =
 	(userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
-		const refreshToken = refreshTokenHandler(userController);
+		const refreshToken = makeRefreshToken(userController);
 		const setting = get_setting();
 		const setGuest = async () => {
 			setToken(ctx, accessTokenName, null, 0);
@@ -140,7 +140,7 @@ export const createUserMiddleWare =
 		return await refreshToken(ctx, setGuest, next);
 	};
 
-const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
+const makeRefreshToken = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
 	const accessPayload = ctx.cookies.get(accessTokenName);
 	const setting = get_setting();
 	const secretKey = setting.jwt_secretkey;
@@ -200,7 +200,7 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
 	}
 };
 export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
-	const handler = refreshTokenHandler(cntr);
+	const handler = makeRefreshToken(cntr);
 	await handler(ctx, fail, success);
 	async function fail() {
 		const user = ctx.state.user as PayloadInfo;
@@ -242,12 +242,54 @@ export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.C
 	ctx.type = "json";
 };
 
+export function getUserSettingHandler(userController: UserAccessor) {
+	return async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
+		const username = ctx.state.user.username;
+		if (!username) {
+			return sendError(403, "not authorized");
+		}
+		const user = await userController.findUser(username);
+		if (user === undefined) {
+			return sendError(403, "not authorized");
+		}
+		const settings = await user.get_settings();
+		if (settings === undefined) {
+			ctx.body = {};
+			ctx.type = "json";
+			return;
+		}
+		ctx.body = settings;
+		ctx.type = "json";
+		await next();
+	};
+}
+export function setUserSettingHandler(userController: UserAccessor) {
+	return async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
+		const username = ctx.state.user.username;
+		if (!username) {
+			return sendError(403, "not authorized");
+		}
+		const user = await userController.findUser(username);
+		if (user === undefined) {
+			return sendError(403, "not authorized");
+		}
+		const body = ctx.request.body;
+		const settings = body as Record<string, unknown>;
+		await user.set_settings(settings);
+		ctx.body = { ok: true };
+		ctx.type = "json";
+		await next();
+	};
+}
+
 export function createLoginRouter(userController: UserAccessor) {
 	const router = new Router();
-	router.post("/login", createLoginMiddleware(userController));
-	router.post("/logout", LogoutMiddleware);
+	router.post("/login", createLoginHandler(userController));
+	router.post("/logout", LogoutHandler);
 	router.post("/refresh", createRefreshTokenMiddleware(userController));
 	router.post("/reset", resetPasswordMiddleware(userController));
+	router.get("/settings", getUserSettingHandler(userController));
+	router.post("/settings", setUserSettingHandler(userController));
 	return router;
 }
 
diff --git a/packages/server/src/model/user.ts b/packages/server/src/model/user.ts
index fdfff72..b0adc1f 100644
--- a/packages/server/src/model/user.ts
+++ b/packages/server/src/model/user.ts
@@ -1,3 +1,4 @@
+import { UserSetting } from "dbtype";
 import { createHmac, randomBytes } from "node:crypto";
 
 function hashForPassword(salt: string, password: string) {
@@ -41,6 +42,8 @@ export interface UserCreateInput {
 	password: string;
 }
 
+export type IUserSettings = UserSetting;
+
 export interface IUser {
 	readonly username: string;
 	readonly password: Password;
@@ -65,6 +68,18 @@ export interface IUser {
 	 * @param password password to set
 	 */
 	reset_password(password: string): Promise<void>;
+
+	/**
+	 * get user settings
+	 * @returns user settings, or undefined if not set
+	 */
+	get_settings(): Promise<IUserSettings | undefined>;
+	/**
+	 * set user settings
+	 * @param settings user settings to set
+	 * @returns if settings updated, return true
+	 */
+	set_settings(settings: IUserSettings): Promise<boolean>;
 }
 
 export interface UserAccessor {
diff --git a/packages/server/src/permission/permission.ts b/packages/server/src/permission/permission.ts
index 4632ef6..04f96c7 100644
--- a/packages/server/src/permission/permission.ts
+++ b/packages/server/src/permission/permission.ts
@@ -36,20 +36,21 @@ export enum Permission {
 
 export const createPermissionCheckMiddleware =
 	(...permissions: string[]) =>
-	async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
-		const user = ctx.state.user;
-		if (user.username === "admin") {
-			return await next();
-		}
-		const user_permission = user.permission;
-		// if permissions is not subset of user permission
-		if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) {
-			if (user.username === "") {
-				return sendError(401, "you are guest. login needed.");
-			}return sendError(403, "do not have permission");
-		}
-		await next();
-	};
+		async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
+			const user = ctx.state.user;
+			if (user.username === "admin") {
+				return await next();
+			}
+			const user_permission = user.permission;
+			// if permissions is not subset of user permission
+			if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) {
+				if (user.username === "") {
+					return sendError(401, "you are guest. login needed.");
+				} return sendError(403, "do not have permission");
+			}
+			await next();
+		};
+
 export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 	const user = ctx.state.user;
 	if (user.username !== "admin") {
diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts
index 6726d30..6ba4037 100644
--- a/packages/server/src/server.ts
+++ b/packages/server/src/server.ts
@@ -8,7 +8,7 @@ import { get_setting, SettingConfig } from "./SettingConfig.ts";
 import { createReadStream, readFileSync } from "node:fs";
 import bodyparser from "koa-bodyparser";
 import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod.ts";
-import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login.ts";
+import { createLoginRouter, createUserHandler, getAdmin, isAdminFirst } from "./login.ts";
 import getContentRouter from "./route/contents.ts";
 import { error_handler } from "./route/error_handler.ts";
 
@@ -63,7 +63,7 @@ class ServerApplication {
 		}
 		app.use(bodyparser());
 		app.use(error_handler);
-		app.use(createUserMiddleWare(this.userController));
+		app.use(createUserHandler(this.userController));
 
 		const diff_router = createDiffRouter(this.diffManger);
 		this.diffManger.register("comic", createComicWatcher());