diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts
index 5cfd6a8b98..690bfae744 100644
--- a/e2e/src/api/specs/server-info.e2e-spec.ts
+++ b/e2e/src/api/specs/server-info.e2e-spec.ts
@@ -1,4 +1,4 @@
-import { LoginResponseDto, getServerConfig } from '@immich/sdk';
+import { LoginResponseDto } from '@immich/sdk';
 import { createUserDto } from 'src/fixtures';
 import { errorDto } from 'src/responses';
 import { app, utils } from 'src/utils';
@@ -162,19 +162,4 @@ describe('/server-info', () => {
       });
     });
   });
-
-  describe('POST /server-info/admin-onboarding', () => {
-    it('should set admin onboarding', async () => {
-      const config = await getServerConfig({});
-      expect(config.isOnboarded).toBe(false);
-
-      const { status } = await request(app)
-        .post('/server-info/admin-onboarding')
-        .set('Authorization', `Bearer ${admin.accessToken}`);
-      expect(status).toBe(204);
-
-      const newConfig = await getServerConfig({});
-      expect(newConfig.isOnboarded).toBe(true);
-    });
-  });
 });
diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/api/specs/system-metadata.e2e-spec.ts
new file mode 100644
index 0000000000..bd17bf2524
--- /dev/null
+++ b/e2e/src/api/specs/system-metadata.e2e-spec.ts
@@ -0,0 +1,76 @@
+import { LoginResponseDto, getServerConfig } from '@immich/sdk';
+import { createUserDto } from 'src/fixtures';
+import { errorDto } from 'src/responses';
+import { app, utils } from 'src/utils';
+import request from 'supertest';
+import { beforeAll, describe, expect, it } from 'vitest';
+
+describe('/server-info', () => {
+  let admin: LoginResponseDto;
+  let nonAdmin: LoginResponseDto;
+
+  beforeAll(async () => {
+    await utils.resetDatabase();
+    admin = await utils.adminSetup({ onboarding: false });
+    nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
+  });
+
+  describe('POST /system-metadata/admin-onboarding', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true });
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should only work for admins', async () => {
+      const { status, body } = await request(app)
+        .post('/system-metadata/admin-onboarding')
+        .set('Authorization', `Bearer ${nonAdmin.accessToken}`)
+        .send({ isOnboarded: true });
+      expect(status).toBe(403);
+      expect(body).toEqual(errorDto.forbidden);
+    });
+
+    it('should set admin onboarding', async () => {
+      const config = await getServerConfig({});
+      expect(config.isOnboarded).toBe(false);
+
+      const { status } = await request(app)
+        .post('/system-metadata/admin-onboarding')
+        .set('Authorization', `Bearer ${admin.accessToken}`)
+        .send({ isOnboarded: true });
+      expect(status).toBe(204);
+
+      const newConfig = await getServerConfig({});
+      expect(newConfig.isOnboarded).toBe(true);
+    });
+  });
+
+  describe('GET /system-metadata/reverse-geocoding-state', () => {
+    it('should require authentication', async () => {
+      const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state');
+      expect(status).toBe(401);
+      expect(body).toEqual(errorDto.unauthorized);
+    });
+
+    it('should only work for admins', async () => {
+      const { status, body } = await request(app)
+        .get('/system-metadata/reverse-geocoding-state')
+        .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
+      expect(status).toBe(403);
+      expect(body).toEqual(errorDto.forbidden);
+    });
+
+    it('should get the reverse geocoding state', async () => {
+      const { status, body } = await request(app)
+        .get('/system-metadata/reverse-geocoding-state')
+        .set('Authorization', `Bearer ${admin.accessToken}`);
+
+      expect(status).toBe(200);
+      expect(body).toEqual({
+        lastUpdate: expect.any(String),
+        lastImportFileName: 'cities500.txt',
+      });
+    });
+  });
+});
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index 0047502023..96994c7f0a 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -24,8 +24,8 @@ import {
   getConfigDefaults,
   login,
   searchMetadata,
-  setAdminOnboarding,
   signUpAdmin,
+  updateAdminOnboarding,
   updateConfig,
   validate,
 } from '@immich/sdk';
@@ -264,7 +264,10 @@ export const utils = {
     await signUpAdmin({ signUpDto: signupDto.admin });
     const response = await login({ loginCredentialDto: loginDto.admin });
     if (options.onboarding) {
-      await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
+      await updateAdminOnboarding(
+        { adminOnboardingUpdateDto: { isOnboarded: true } },
+        { headers: asBearerAuth(response.accessToken) },
+      );
     }
     return response;
   },
diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index 42f1034dce..64229329aa 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -13,6 +13,7 @@ doc/ActivityCreateDto.md
 doc/ActivityResponseDto.md
 doc/ActivityStatisticsResponseDto.md
 doc/AddUsersDto.md
+doc/AdminOnboardingUpdateDto.md
 doc/AlbumApi.md
 doc/AlbumCountResponseDto.md
 doc/AlbumResponseDto.md
@@ -123,6 +124,7 @@ doc/QueueStatusDto.md
 doc/ReactionLevel.md
 doc/ReactionType.md
 doc/RecognitionConfig.md
+doc/ReverseGeocodingStateResponseDto.md
 doc/ScanLibraryDto.md
 doc/SearchAlbumResponseDto.md
 doc/SearchApi.md
@@ -174,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md
 doc/SystemConfigThemeDto.md
 doc/SystemConfigTrashDto.md
 doc/SystemConfigUserDto.md
+doc/SystemMetadataApi.md
 doc/TagApi.md
 doc/TagResponseDto.md
 doc/TagTypeEnum.md
@@ -226,6 +229,7 @@ lib/api/sessions_api.dart
 lib/api/shared_link_api.dart
 lib/api/sync_api.dart
 lib/api/system_config_api.dart
+lib/api/system_metadata_api.dart
 lib/api/tag_api.dart
 lib/api/timeline_api.dart
 lib/api/trash_api.dart
@@ -242,6 +246,7 @@ lib/model/activity_create_dto.dart
 lib/model/activity_response_dto.dart
 lib/model/activity_statistics_response_dto.dart
 lib/model/add_users_dto.dart
+lib/model/admin_onboarding_update_dto.dart
 lib/model/album_count_response_dto.dart
 lib/model/album_response_dto.dart
 lib/model/all_job_status_response_dto.dart
@@ -343,6 +348,7 @@ lib/model/queue_status_dto.dart
 lib/model/reaction_level.dart
 lib/model/reaction_type.dart
 lib/model/recognition_config.dart
+lib/model/reverse_geocoding_state_response_dto.dart
 lib/model/scan_library_dto.dart
 lib/model/search_album_response_dto.dart
 lib/model/search_asset_response_dto.dart
@@ -419,6 +425,7 @@ test/activity_create_dto_test.dart
 test/activity_response_dto_test.dart
 test/activity_statistics_response_dto_test.dart
 test/add_users_dto_test.dart
+test/admin_onboarding_update_dto_test.dart
 test/album_api_test.dart
 test/album_count_response_dto_test.dart
 test/album_response_dto_test.dart
@@ -534,6 +541,7 @@ test/queue_status_dto_test.dart
 test/reaction_level_test.dart
 test/reaction_type_test.dart
 test/recognition_config_test.dart
+test/reverse_geocoding_state_response_dto_test.dart
 test/scan_library_dto_test.dart
 test/search_album_response_dto_test.dart
 test/search_api_test.dart
@@ -585,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart
 test/system_config_theme_dto_test.dart
 test/system_config_trash_dto_test.dart
 test/system_config_user_dto_test.dart
+test/system_metadata_api_test.dart
 test/tag_api_test.dart
 test/tag_response_dto_test.dart
 test/tag_type_enum_test.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 3ebd65025b..27d631e4fd 100644
Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ
diff --git a/mobile/openapi/doc/AdminOnboardingUpdateDto.md b/mobile/openapi/doc/AdminOnboardingUpdateDto.md
new file mode 100644
index 0000000000..b250843019
Binary files /dev/null and b/mobile/openapi/doc/AdminOnboardingUpdateDto.md differ
diff --git a/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md
new file mode 100644
index 0000000000..87f8aa8ab7
Binary files /dev/null and b/mobile/openapi/doc/ReverseGeocodingStateResponseDto.md differ
diff --git a/mobile/openapi/doc/ServerInfoApi.md b/mobile/openapi/doc/ServerInfoApi.md
index cb5cf0fd3e..e8121a8001 100644
Binary files a/mobile/openapi/doc/ServerInfoApi.md and b/mobile/openapi/doc/ServerInfoApi.md differ
diff --git a/mobile/openapi/doc/SystemMetadataApi.md b/mobile/openapi/doc/SystemMetadataApi.md
new file mode 100644
index 0000000000..f8c2347afe
Binary files /dev/null and b/mobile/openapi/doc/SystemMetadataApi.md differ
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 8520bab305..44bd35a683 100644
Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ
diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart
index 77840acd19..b67045add1 100644
Binary files a/mobile/openapi/lib/api/server_info_api.dart and b/mobile/openapi/lib/api/server_info_api.dart differ
diff --git a/mobile/openapi/lib/api/system_metadata_api.dart b/mobile/openapi/lib/api/system_metadata_api.dart
new file mode 100644
index 0000000000..f3952fda8a
Binary files /dev/null and b/mobile/openapi/lib/api/system_metadata_api.dart differ
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 0a0cd80088..a92f1df7a7 100644
Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ
diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart
new file mode 100644
index 0000000000..50c4ae090e
Binary files /dev/null and b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart differ
diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart
new file mode 100644
index 0000000000..71e1d3ad99
Binary files /dev/null and b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart differ
diff --git a/mobile/openapi/test/admin_onboarding_update_dto_test.dart b/mobile/openapi/test/admin_onboarding_update_dto_test.dart
new file mode 100644
index 0000000000..09cc73e977
Binary files /dev/null and b/mobile/openapi/test/admin_onboarding_update_dto_test.dart differ
diff --git a/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart
new file mode 100644
index 0000000000..91fdfcfea4
Binary files /dev/null and b/mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart differ
diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart
index 68cd1c348b..dac465116e 100644
Binary files a/mobile/openapi/test/server_info_api_test.dart and b/mobile/openapi/test/server_info_api_test.dart differ
diff --git a/mobile/openapi/test/system_metadata_api_test.dart b/mobile/openapi/test/system_metadata_api_test.dart
new file mode 100644
index 0000000000..bc1ce6f6f3
Binary files /dev/null and b/mobile/openapi/test/system_metadata_api_test.dart differ
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index f49df7baea..4f666b303c 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -4908,31 +4908,6 @@
         ]
       }
     },
-    "/server-info/admin-onboarding": {
-      "post": {
-        "operationId": "setAdminOnboarding",
-        "parameters": [],
-        "responses": {
-          "204": {
-            "description": ""
-          }
-        },
-        "security": [
-          {
-            "bearer": []
-          },
-          {
-            "cookie": []
-          },
-          {
-            "api_key": []
-          }
-        ],
-        "tags": [
-          "Server Info"
-        ]
-      }
-    },
     "/server-info/config": {
       "get": {
         "operationId": "getServerConfig",
@@ -5885,6 +5860,103 @@
         ]
       }
     },
+    "/system-metadata/admin-onboarding": {
+      "get": {
+        "operationId": "getAdminOnboarding",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/AdminOnboardingUpdateDto"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "System Metadata"
+        ]
+      },
+      "post": {
+        "operationId": "updateAdminOnboarding",
+        "parameters": [],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/AdminOnboardingUpdateDto"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "204": {
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "System Metadata"
+        ]
+      }
+    },
+    "/system-metadata/reverse-geocoding-state": {
+      "get": {
+        "operationId": "getReverseGeocodingState",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/ReverseGeocodingStateResponseDto"
+                }
+              }
+            },
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "System Metadata"
+        ]
+      }
+    },
     "/tag": {
       "get": {
         "operationId": "getAllTags",
@@ -7180,6 +7252,17 @@
         ],
         "type": "object"
       },
+      "AdminOnboardingUpdateDto": {
+        "properties": {
+          "isOnboarded": {
+            "type": "boolean"
+          }
+        },
+        "required": [
+          "isOnboarded"
+        ],
+        "type": "object"
+      },
       "AlbumCountResponseDto": {
         "properties": {
           "notShared": {
@@ -9618,6 +9701,23 @@
         ],
         "type": "object"
       },
+      "ReverseGeocodingStateResponseDto": {
+        "properties": {
+          "lastImportFileName": {
+            "nullable": true,
+            "type": "string"
+          },
+          "lastUpdate": {
+            "nullable": true,
+            "type": "string"
+          }
+        },
+        "required": [
+          "lastImportFileName",
+          "lastUpdate"
+        ],
+        "type": "object"
+      },
       "ScanLibraryDto": {
         "properties": {
           "refreshAllFiles": {
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 9148b4d3b1..1bf219162d 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -998,6 +998,13 @@ export type SystemConfigTemplateStorageOptionDto = {
     weekOptions: string[];
     yearOptions: string[];
 };
+export type AdminOnboardingUpdateDto = {
+    isOnboarded: boolean;
+};
+export type ReverseGeocodingStateResponseDto = {
+    lastImportFileName: string | null;
+    lastUpdate: string | null;
+};
 export type CreateTagDto = {
     name: string;
     "type": TagTypeEnum;
@@ -2330,12 +2337,6 @@ export function getServerInfo(opts?: Oazapfts.RequestOpts) {
         ...opts
     }));
 }
-export function setAdminOnboarding(opts?: Oazapfts.RequestOpts) {
-    return oazapfts.ok(oazapfts.fetchText("/server-info/admin-onboarding", {
-        ...opts,
-        method: "POST"
-    }));
-}
 export function getServerConfig(opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
@@ -2597,6 +2598,31 @@ export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) {
         ...opts
     }));
 }
+export function getAdminOnboarding(opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: AdminOnboardingUpdateDto;
+    }>("/system-metadata/admin-onboarding", {
+        ...opts
+    }));
+}
+export function updateAdminOnboarding({ adminOnboardingUpdateDto }: {
+    adminOnboardingUpdateDto: AdminOnboardingUpdateDto;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchText("/system-metadata/admin-onboarding", oazapfts.json({
+        ...opts,
+        method: "POST",
+        body: adminOnboardingUpdateDto
+    })));
+}
+export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: ReverseGeocodingStateResponseDto;
+    }>("/system-metadata/reverse-geocoding-state", {
+        ...opts
+    }));
+}
 export function getAllTags(opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts
index ad2f6e8de1..bd10c41a43 100644
--- a/server/src/controllers/index.ts
+++ b/server/src/controllers/index.ts
@@ -21,6 +21,7 @@ import { SessionController } from 'src/controllers/session.controller';
 import { SharedLinkController } from 'src/controllers/shared-link.controller';
 import { SyncController } from 'src/controllers/sync.controller';
 import { SystemConfigController } from 'src/controllers/system-config.controller';
+import { SystemMetadataController } from 'src/controllers/system-metadata.controller';
 import { TagController } from 'src/controllers/tag.controller';
 import { TimelineController } from 'src/controllers/timeline.controller';
 import { TrashController } from 'src/controllers/trash.controller';
@@ -51,6 +52,7 @@ export const controllers = [
   SharedLinkController,
   SyncController,
   SystemConfigController,
+  SystemMetadataController,
   TagController,
   TimelineController,
   TrashController,
diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts
index e32b0d191c..35e5e17594 100644
--- a/server/src/controllers/server-info.controller.ts
+++ b/server/src/controllers/server-info.controller.ts
@@ -1,4 +1,4 @@
-import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
+import { Controller, Get } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
 import {
   ServerConfigDto,
@@ -65,11 +65,4 @@ export class ServerInfoController {
   getSupportedMediaTypes(): ServerMediaTypesResponseDto {
     return this.service.getSupportedMediaTypes();
   }
-
-  @AdminRoute()
-  @Post('admin-onboarding')
-  @HttpCode(HttpStatus.NO_CONTENT)
-  setAdminOnboarding(): Promise<void> {
-    return this.service.setAdminOnboarding();
-  }
 }
diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts
new file mode 100644
index 0000000000..7f186fec03
--- /dev/null
+++ b/server/src/controllers/system-metadata.controller.ts
@@ -0,0 +1,28 @@
+import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
+import { ApiTags } from '@nestjs/swagger';
+import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto';
+import { Authenticated } from 'src/middleware/auth.guard';
+import { SystemMetadataService } from 'src/services/system-metadata.service';
+
+@ApiTags('System Metadata')
+@Controller('system-metadata')
+@Authenticated({ admin: true })
+export class SystemMetadataController {
+  constructor(private service: SystemMetadataService) {}
+
+  @Get('admin-onboarding')
+  getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
+    return this.service.getAdminOnboarding();
+  }
+
+  @Post('admin-onboarding')
+  @HttpCode(HttpStatus.NO_CONTENT)
+  updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
+    return this.service.updateAdminOnboarding(dto);
+  }
+
+  @Get('reverse-geocoding-state')
+  getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
+    return this.service.getReverseGeocodingState();
+  }
+}
diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts
new file mode 100644
index 0000000000..1c04435341
--- /dev/null
+++ b/server/src/dtos/system-metadata.dto.ts
@@ -0,0 +1,15 @@
+import { IsBoolean } from 'class-validator';
+
+export class AdminOnboardingUpdateDto {
+  @IsBoolean()
+  isOnboarded!: boolean;
+}
+
+export class AdminOnboardingResponseDto {
+  isOnboarded!: boolean;
+}
+
+export class ReverseGeocodingStateResponseDto {
+  lastUpdate!: string | null;
+  lastImportFileName!: string | null;
+}
diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts
index 8eeb0064ac..e7d37407d9 100644
--- a/server/src/repositories/metadata.repository.ts
+++ b/server/src/repositories/metadata.repository.ts
@@ -36,8 +36,8 @@ export class MetadataRepository implements IMetadataRepository {
     this.logger.log('Initializing metadata repository');
     const geodataDate = await readFile(geodataDatePath, 'utf8');
 
+    // TODO move to metadata service init
     const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
-
     if (geocodingMetadata?.lastUpdate === geodataDate) {
       return;
     }
diff --git a/server/src/services/index.ts b/server/src/services/index.ts
index db3d6083e9..2305708caa 100644
--- a/server/src/services/index.ts
+++ b/server/src/services/index.ts
@@ -25,6 +25,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
 import { StorageService } from 'src/services/storage.service';
 import { SyncService } from 'src/services/sync.service';
 import { SystemConfigService } from 'src/services/system-config.service';
+import { SystemMetadataService } from 'src/services/system-metadata.service';
 import { TagService } from 'src/services/tag.service';
 import { TimelineService } from 'src/services/timeline.service';
 import { TrashService } from 'src/services/trash.service';
@@ -58,6 +59,7 @@ export const services = [
   StorageTemplateService,
   SyncService,
   SystemConfigService,
+  SystemMetadataService,
   TagService,
   TimelineService,
   TrashService,
diff --git a/server/src/services/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts
index 836909b74f..115ab4b6a1 100644
--- a/server/src/services/server-info.service.spec.ts
+++ b/server/src/services/server-info.service.spec.ts
@@ -1,5 +1,4 @@
 import { serverVersion } from 'src/constants';
-import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
 import { IEventRepository } from 'src/interfaces/event.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
@@ -207,13 +206,6 @@ describe(ServerInfoService.name, () => {
     });
   });
 
-  describe('setAdminOnboarding', () => {
-    it('should set admin onboarding to true', async () => {
-      await sut.setAdminOnboarding();
-      expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
-    });
-  });
-
   describe('getStats', () => {
     it('should total up usage by user', async () => {
       userMock.getUserStats.mockResolvedValue([
diff --git a/server/src/services/server-info.service.ts b/server/src/services/server-info.service.ts
index bb092896bf..52bf8bd1d3 100644
--- a/server/src/services/server-info.service.ts
+++ b/server/src/services/server-info.service.ts
@@ -51,7 +51,9 @@ export class ServerInfoService {
 
     const featureFlags = await this.getFeatures();
     if (featureFlags.configFile) {
-      await this.setAdminOnboarding();
+      await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
+        isOnboarded: true,
+      });
     }
   }
 
@@ -105,10 +107,6 @@ export class ServerInfoService {
     };
   }
 
-  setAdminOnboarding(): Promise<void> {
-    return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
-  }
-
   async getStatistics(): Promise<ServerStatsResponseDto> {
     const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
     const serverStats = new ServerStatsResponseDto();
diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts
new file mode 100644
index 0000000000..9d11c1c72a
--- /dev/null
+++ b/server/src/services/system-metadata.service.spec.ts
@@ -0,0 +1,31 @@
+import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { SystemMetadataService } from 'src/services/system-metadata.service';
+import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
+import { Mocked } from 'vitest';
+
+describe(SystemMetadataService.name, () => {
+  let sut: SystemMetadataService;
+  let metadataMock: Mocked<ISystemMetadataRepository>;
+
+  beforeEach(() => {
+    metadataMock = newSystemMetadataRepositoryMock();
+    sut = new SystemMetadataService(metadataMock);
+  });
+
+  it('should work', () => {
+    expect(sut).toBeDefined();
+  });
+
+  describe('updateAdminOnboarding', () => {
+    it('should update isOnboarded to true', async () => {
+      await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined();
+      expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
+    });
+
+    it('should update isOnboarded to false', async () => {
+      await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined();
+      expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false });
+    });
+  });
+});
diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts
new file mode 100644
index 0000000000..e8fddfc13c
--- /dev/null
+++ b/server/src/services/system-metadata.service.ts
@@ -0,0 +1,29 @@
+import { Inject, Injectable } from '@nestjs/common';
+import {
+  AdminOnboardingResponseDto,
+  AdminOnboardingUpdateDto,
+  ReverseGeocodingStateResponseDto,
+} from 'src/dtos/system-metadata.dto';
+import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+
+@Injectable()
+export class SystemMetadataService {
+  constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {}
+
+  async getAdminOnboarding(): Promise<AdminOnboardingResponseDto> {
+    const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING);
+    return { isOnboarded: false, ...value };
+  }
+
+  async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise<void> {
+    await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
+      isOnboarded: dto.isOnboarded,
+    });
+  }
+
+  async getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
+    const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
+    return { lastUpdate: null, lastImportFileName: null, ...value };
+  }
+}
diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte
index 09139a7f7e..4647ad8bde 100644
--- a/web/src/routes/auth/onboarding/+page.svelte
+++ b/web/src/routes/auth/onboarding/+page.svelte
@@ -5,7 +5,7 @@
   import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte';
   import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte';
   import { AppRoute, QueryParameter } from '$lib/constants';
-  import { setAdminOnboarding } from '@immich/sdk';
+  import { updateAdminOnboarding } from '@immich/sdk';
 
   let index = 0;
 
@@ -28,7 +28,7 @@
 
   const handleDoneClicked = async () => {
     if (index >= onboardingSteps.length - 1) {
-      await setAdminOnboarding();
+      await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } });
       await goto(AppRoute.PHOTOS);
     } else {
       index++;