diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx
index 157c31c89ac9458c167da161a62ef917176f7754..c9abf7956f16a04d14f8ea1b17da36804339df68 100644
--- a/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx
+++ b/packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx
@@ -31,6 +31,9 @@ vi.mock('loot-core/src/platform/client/fetch');
 vi.mock('../../hooks/useFeatureFlag', () => ({
   default: vi.fn().mockReturnValue(false),
 }));
+vi.mock('../../hooks/useSyncedPref', () => ({
+  useSyncedPref: vi.fn().mockReturnValue([undefined, vi.fn()]),
+}));
 
 const accounts = [generateAccount('Bank of America')];
 const payees = [
diff --git a/packages/desktop-client/src/hooks/useLocalPrefs.ts b/packages/desktop-client/src/hooks/useLocalPrefs.ts
deleted file mode 100644
index 870bef808e340661745e538aa3b619e91c60e34d..0000000000000000000000000000000000000000
--- a/packages/desktop-client/src/hooks/useLocalPrefs.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { useSelector } from 'react-redux';
-
-import { type State } from 'loot-core/src/client/state-types';
-
-export function useLocalPrefs() {
-  return useSelector((state: State) => state.prefs.local);
-}
diff --git a/packages/desktop-client/src/hooks/useSyncedPref.ts b/packages/desktop-client/src/hooks/useSyncedPref.ts
index e9e8572300e7cdd2bac64af07108978942a86717..5e226e6069298cb55e83a2e62ee3d3a77c6991dd 100644
--- a/packages/desktop-client/src/hooks/useSyncedPref.ts
+++ b/packages/desktop-client/src/hooks/useSyncedPref.ts
@@ -1,6 +1,9 @@
-import { type SyncedPrefs } from 'loot-core/src/types/prefs';
+import { useCallback } from 'react';
 
-import { useLocalPref } from './useLocalPref';
+import { useQuery } from 'loot-core/client/query-hooks';
+import { send } from 'loot-core/platform/client/fetch';
+import { q } from 'loot-core/shared/query';
+import { type SyncedPrefs } from 'loot-core/src/types/prefs';
 
 type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
   value: SyncedPrefs[K],
@@ -9,7 +12,21 @@ type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
 export function useSyncedPref<K extends keyof SyncedPrefs>(
   prefName: K,
 ): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
-  // TODO: implement logic for fetching the pref exclusively from the
-  // database (in follow-up PR)
-  return useLocalPref(prefName);
+  const { data: queryData, overrideData: setQueryData } = useQuery<
+    [{ value: string | undefined }]
+  >(
+    () => q('preferences').filter({ id: prefName }).select('value'),
+    [prefName],
+  );
+
+  const setLocalPref = useCallback<SetSyncedPrefAction<K>>(
+    newValue => {
+      const value = String(newValue);
+      setQueryData([{ value }]);
+      send('preferences/save', { id: prefName, value });
+    },
+    [prefName, setQueryData],
+  );
+
+  return [queryData?.[0]?.value, setLocalPref];
 }
diff --git a/packages/desktop-client/src/hooks/useSyncedPrefs.ts b/packages/desktop-client/src/hooks/useSyncedPrefs.ts
index 7d4497da649258f2a1c929228506239c43f1d313..60478520229ee32084b3085865154492ff820fd7 100644
--- a/packages/desktop-client/src/hooks/useSyncedPrefs.ts
+++ b/packages/desktop-client/src/hooks/useSyncedPrefs.ts
@@ -1,22 +1,39 @@
-import { useCallback } from 'react';
-import { useDispatch } from 'react-redux';
+import { useCallback, useMemo } from 'react';
 
-import { savePrefs } from 'loot-core/client/actions';
+import { useQuery } from 'loot-core/client/query-hooks';
+import { send } from 'loot-core/platform/client/fetch';
+import { q } from 'loot-core/shared/query';
 import { type SyncedPrefs } from 'loot-core/src/types/prefs';
 
-import { useLocalPrefs } from './useLocalPrefs';
-
 type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
 
+/** @deprecated: please use `useSyncedPref` (singular) */
 export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
-  // TODO: implement real logic (follow-up PR)
-  const dispatch = useDispatch();
-  const setPrefs = useCallback<SetSyncedPrefsAction>(
-    newPrefs => {
-      dispatch(savePrefs(newPrefs));
-    },
-    [dispatch],
+  const { data: queryData } = useQuery<{ id: string; value: string }[]>(
+    () => q('preferences').select(['id', 'value']),
+    [],
+  );
+
+  const prefs = useMemo<SyncedPrefs>(
+    () =>
+      queryData.reduce(
+        (carry, { id, value }) => ({
+          ...carry,
+          [id]: value,
+        }),
+        {},
+      ),
+    [queryData],
   );
 
-  return [useLocalPrefs(), setPrefs];
+  const setPrefs = useCallback<SetSyncedPrefsAction>(newValue => {
+    Object.entries(newValue).forEach(([id, value]) => {
+      send('preferences/save', {
+        id: id as keyof SyncedPrefs,
+        value: String(value),
+      });
+    });
+  }, []);
+
+  return [prefs, setPrefs];
 }
diff --git a/packages/loot-core/migrations/1723665565000_prefs.js b/packages/loot-core/migrations/1723665565000_prefs.js
new file mode 100644
index 0000000000000000000000000000000000000000..cd52e78a226d883dc085f8aa1631afd6caffa51f
--- /dev/null
+++ b/packages/loot-core/migrations/1723665565000_prefs.js
@@ -0,0 +1,59 @@
+const SYNCED_PREF_KEYS = [
+  'firstDayOfWeekIdx',
+  'dateFormat',
+  'numberFormat',
+  'hideFraction',
+  'isPrivacyEnabled',
+  /^show-extra-balances-/,
+  /^hide-cleared-/,
+  /^parse-date-/,
+  /^csv-mappings-/,
+  /^csv-delimiter-/,
+  /^csv-has-header-/,
+  /^ofx-fallback-missing-payee-/,
+  /^flip-amount-/,
+  // 'budgetType', // TODO: uncomment when `budgetType` moves from metadata to synced prefs
+  /^flags\./,
+];
+
+export default async function runMigration(db, { fs, fileId }) {
+  await db.execQuery(`
+    CREATE TABLE preferences
+       (id TEXT PRIMARY KEY,
+        value TEXT);
+  `);
+
+  try {
+    const budgetDir = fs.getBudgetDir(fileId);
+    const fullpath = fs.join(budgetDir, 'metadata.json');
+
+    const prefs = JSON.parse(await fs.readFile(fullpath));
+
+    if (typeof prefs !== 'object') {
+      return;
+    }
+
+    await Promise.all(
+      Object.keys(prefs).map(async key => {
+        // Check if the current key is of synced-keys type
+        if (
+          !SYNCED_PREF_KEYS.find(keyMatcher =>
+            keyMatcher instanceof RegExp
+              ? keyMatcher.test(key)
+              : keyMatcher === key,
+          )
+        ) {
+          return;
+        }
+
+        // insert the synced prefs in the new table
+        await db.runQuery('INSERT INTO preferences (id, value) VALUES (?, ?)', [
+          key,
+          String(prefs[key]),
+        ]);
+      }),
+    );
+  } catch (e) {
+    // Do nothing
+  }
+}
diff --git a/packages/loot-core/src/client/query-helpers.ts b/packages/loot-core/src/client/query-helpers.ts
index 0f8963c0181582437e2706163ef8880c9b6f3d2e..92f058b9f5e79956987e801e5a4787423d735af8 100644
--- a/packages/loot-core/src/client/query-helpers.ts
+++ b/packages/loot-core/src/client/query-helpers.ts
@@ -7,8 +7,12 @@ export async function runQuery(query) {
   return send('query', query.serialize());
 }
 
-export function liveQuery(query, onData?, opts?): LiveQuery {
-  const q = new LiveQuery(query, onData, opts);
+export function liveQuery<Response = unknown>(
+  query,
+  onData?: (response: Response) => void,
+  opts?,
+): LiveQuery {
+  const q = new LiveQuery<Response>(query, onData, opts);
   q.run();
   return q;
 }
@@ -20,7 +24,7 @@ export function pagedQuery(query, onData?, opts?): PagedQuery {
 }
 
 // Subscribe and refetch
-export class LiveQuery {
+export class LiveQuery<Response = unknown> {
   _unsubscribe;
   data;
   dependencies;
@@ -36,7 +40,11 @@ export class LiveQuery {
   inflightRequestId;
   restart;
 
-  constructor(query, onData?, opts: { mapper?; onlySync?: boolean } = {}) {
+  constructor(
+    query,
+    onData?: (response: Response) => void,
+    opts: { mapper?; onlySync?: boolean } = {},
+  ) {
     this.error = new Error();
     this.query = query;
     this.data = null;
diff --git a/packages/loot-core/src/client/query-hooks.tsx b/packages/loot-core/src/client/query-hooks.tsx
index ecfeddfd4d3df67defaf21867ab9fe3aeef61412..930acfc58751c7d474f69de508f9281b82053fd7 100644
--- a/packages/loot-core/src/client/query-hooks.tsx
+++ b/packages/loot-core/src/client/query-hooks.tsx
@@ -74,21 +74,42 @@ export function useLiveQuery<Response = unknown>(
   makeQuery: () => Query,
   deps: DependencyList,
 ): Response {
-  const [data, setData] = useState(null);
+  const { data } = useQuery<Response>(makeQuery, deps);
+  return data;
+}
+
+export function useQuery<Response = unknown>(
+  makeQuery: () => Query,
+  deps: DependencyList,
+): {
+  data: Response;
+  overrideData: (newData: Response) => void;
+  isLoading: boolean;
+} {
+  const [data, setData] = useState<null | Response>(null);
+  const [isLoading, setIsLoading] = useState(true);
   const query = useMemo(makeQuery, deps);
 
   useEffect(() => {
-    let live = liveQuery(query, async data => {
+    setIsLoading(true);
+
+    let live = liveQuery<Response>(query, async data => {
       if (live) {
+        setIsLoading(false);
         setData(data);
       }
     });
 
     return () => {
+      setIsLoading(false);
       live.unsubscribe();
       live = null;
     };
   }, [query]);
 
-  return data;
+  return {
+    data,
+    overrideData: setData,
+    isLoading,
+  };
 }
diff --git a/packages/loot-core/src/server/aql/schema/index.ts b/packages/loot-core/src/server/aql/schema/index.ts
index b35e2a25a5fac4ef9f766d333e6f3ea9387f3e58..ddcac48056a480b2112fc4e21334d4f9b57617b5 100644
--- a/packages/loot-core/src/server/aql/schema/index.ts
+++ b/packages/loot-core/src/server/aql/schema/index.ts
@@ -122,6 +122,10 @@ export const schema = {
     id: f('id'),
     note: f('string'),
   },
+  preferences: {
+    id: f('id'),
+    value: f('string'),
+  },
   transaction_filters: {
     id: f('id'),
     name: f('string'),
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index c8dcd9f687455d51d2107a5536e58ce21d254734..f3b84fa9a3527825f07c1a58bef4f3f4ab1c5dd7 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -53,6 +53,7 @@ import { mutator, runHandler } from './mutators';
 import { app as notesApp } from './notes/app';
 import * as Platform from './platform';
 import { get, post } from './post';
+import { app as preferencesApp } from './preferences/app';
 import * as prefs from './prefs';
 import { app as reportsApp } from './reports/app';
 import { app as rulesApp } from './rules/app';
@@ -2079,6 +2080,7 @@ app.combine(
   budgetApp,
   dashboardApp,
   notesApp,
+  preferencesApp,
   toolsApp,
   filtersApp,
   reportsApp,
diff --git a/packages/loot-core/src/server/migrate/migrations.ts b/packages/loot-core/src/server/migrate/migrations.ts
index 4243b6a48357d34de72c0652e103f0e9b09a4d8d..3d933e7952407b9147a8190525df96a800c8c2ca 100644
--- a/packages/loot-core/src/server/migrate/migrations.ts
+++ b/packages/loot-core/src/server/migrate/migrations.ts
@@ -3,13 +3,14 @@
 // them which doesn't play well with CSP. There isn't great, and eventually
 // we can remove this migration.
 import { Database } from '@jlongster/sql.js';
-import { v4 as uuidv4 } from 'uuid';
 
 import m1632571489012 from '../../../migrations/1632571489012_remove_cache';
 import m1722717601000 from '../../../migrations/1722717601000_reports_move_selected_categories';
 import m1722804019000 from '../../../migrations/1722804019000_create_dashboard_table';
+import m1723665565000 from '../../../migrations/1723665565000_prefs';
 import * as fs from '../../platform/server/fs';
 import * as sqlite from '../../platform/server/sqlite';
+import * as prefs from '../prefs';
 
 let MIGRATIONS_DIR = fs.migrationsPath;
 
@@ -17,6 +18,7 @@ const javascriptMigrations = {
   1632571489012: m1632571489012,
   1722717601000: m1722717601000,
   1722804019000: m1722804019000,
+  1723665565000: m1723665565000,
 };
 
 export async function withMigrationsDir(
@@ -107,7 +109,10 @@ async function applyJavaScript(db, id) {
   }
 
   const run = javascriptMigrations[id];
-  return run(dbInterface, () => uuidv4());
+  return run(dbInterface, {
+    fs,
+    fileId: prefs.getPrefs()?.id,
+  });
 }
 
 async function applySql(db, sql) {
diff --git a/packages/loot-core/src/server/preferences/app.ts b/packages/loot-core/src/server/preferences/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d4f6517e8d63a40e55425c75c73c333887cef8b
--- /dev/null
+++ b/packages/loot-core/src/server/preferences/app.ts
@@ -0,0 +1,21 @@
+import { type SyncedPrefs } from '../../types/prefs';
+import { createApp } from '../app';
+import * as db from '../db';
+import { mutator } from '../mutators';
+import { undoable } from '../undo';
+
+import { PreferencesHandlers } from './types/handlers';
+
+export const app = createApp<PreferencesHandlers>();
+
+const savePreferences = async ({
+  id,
+  value,
+}: {
+  id: keyof SyncedPrefs;
+  value: string | undefined;
+}) => {
+  await db.update('preferences', { id, value });
+};
+
+app.method('preferences/save', mutator(undoable(savePreferences)));
diff --git a/packages/loot-core/src/server/preferences/types/handlers.d.ts b/packages/loot-core/src/server/preferences/types/handlers.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..78e1a811867db17af1e355e4d7096f62a45ba085
--- /dev/null
+++ b/packages/loot-core/src/server/preferences/types/handlers.d.ts
@@ -0,0 +1,8 @@
+import { type SyncedPrefs } from '../../../types/prefs';
+
+export interface PreferencesHandlers {
+  'preferences/save': (arg: {
+    id: keyof SyncedPrefs;
+    value: string | undefined;
+  }) => Promise<void>;
+}
diff --git a/packages/loot-core/src/server/prefs.ts b/packages/loot-core/src/server/prefs.ts
index e18d107d3c4a96be96b985b76da53b2eb0e5057d..b353acfbe34c87dafbeac7bf6df5108c5320630d 100644
--- a/packages/loot-core/src/server/prefs.ts
+++ b/packages/loot-core/src/server/prefs.ts
@@ -28,19 +28,6 @@ export async function loadPrefs(id?: string): Promise<MetadataPrefs> {
     prefs = { id, budgetName: id };
   }
 
-  // delete released feature flags
-  const releasedFeatures = ['syncAccount'];
-  for (const feature of releasedFeatures) {
-    delete prefs[`flags.${feature}`];
-  }
-
-  // delete legacy notifications
-  for (const key of Object.keys(prefs)) {
-    if (key.startsWith('notifications.')) {
-      delete prefs[key];
-    }
-  }
-
   // No matter what is in `id` field, force it to be the current id.
   // This makes it resilient to users moving around folders, etc
   prefs.id = id;
diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts
index 39d69edbd19840436cc966203573ca03e1f49b7f..ef880e2515b51f6432acaab65706225760122bcb 100644
--- a/packages/loot-core/src/types/handlers.d.ts
+++ b/packages/loot-core/src/types/handlers.d.ts
@@ -2,6 +2,7 @@ import type { BudgetHandlers } from '../server/budget/types/handlers';
 import type { DashboardHandlers } from '../server/dashboard/types/handlers';
 import type { FiltersHandlers } from '../server/filters/types/handlers';
 import type { NotesHandlers } from '../server/notes/types/handlers';
+import type { PreferencesHandlers } from '../server/preferences/types/handlers';
 import type { ReportsHandlers } from '../server/reports/types/handlers';
 import type { RulesHandlers } from '../server/rules/types/handlers';
 import type { SchedulesHandlers } from '../server/schedules/types/handlers';
@@ -17,6 +18,7 @@ export interface Handlers
     DashboardHandlers,
     FiltersHandlers,
     NotesHandlers,
+    PreferencesHandlers,
     ReportsHandlers,
     RulesHandlers,
     SchedulesHandlers,
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index b500745a78c60f05a588c1c187be9c6d492ab3bb..eb0700a57020357c76f818d8d017ebc09f6054b0 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -55,11 +55,10 @@ export type MetadataPrefs = Partial<{
 
 /**
  * Local preferences applicable to a single device. Stored in local storage.
- * TODO: eventually `LocalPrefs` type should not use `SyncedPrefs` or `MetadataPrefs`;
+ * TODO: eventually `LocalPrefs` type should not use `MetadataPrefs`;
  * this is only a stop-gap solution.
  */
-export type LocalPrefs = SyncedPrefs &
-  MetadataPrefs &
+export type LocalPrefs = MetadataPrefs &
   Partial<{
     'ui.showClosedAccounts': boolean;
     'expand-splits': boolean;
diff --git a/upcoming-release-notes/3423.md b/upcoming-release-notes/3423.md
new file mode 100644
index 0000000000000000000000000000000000000000..366c1281f3ea9721ec77d7e5cf88d32cffe4c951
--- /dev/null
+++ b/upcoming-release-notes/3423.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+SyncedPrefs: move synced-preferences from metadata.json to the database.