diff --git a/.terraform-version b/.terraform-version
new file mode 100644
index 0000000000000000000000000000000000000000..9c6d6293b1a8f448def89c2d5bfa63b89a24e0cc
--- /dev/null
+++ b/.terraform-version
@@ -0,0 +1 @@
+1.6.1
diff --git a/README.md b/README.md
index dffc127ed2643fd84bc3d3d17131a41cdb87d9df..c63f69319b80243bc220bf0f733a2c9f905ad998 100644
--- a/README.md
+++ b/README.md
@@ -145,10 +145,9 @@ values.
 In the context of HOLI-8190 recommendations were introduced. Recommendations
 are based on embeddings that were previously created by an ML pipeline and
 stored in a Postgres database within Google Cloud. These are currently not
-locally available. The endpoint `engagementRecommendations` relies on a
-connection to the aforementioned database that is only accessible to holi
-employees (unless running the service with the environment variable
-`FAKE=true`).
+locally available. The endpoints `engagementRecommendations` & `engagementRecos`
+rely on a connection to the aforementioned database that is only accessible to holi
+employees (unless running the service with the environment variable `FAKE=true`).
 
 ### Running
 
diff --git a/app/geo_api_client_test.ts b/app/geo_api_client_test.ts
index 8bb8c5412b64ceaf79274ab13726d0125d8c22e6..58b71f0ebd9bb61e077d78d1265a7e5a752468bf 100644
--- a/app/geo_api_client_test.ts
+++ b/app/geo_api_client_test.ts
@@ -2,6 +2,12 @@ import { stubFetch, stubFetchWithResponses } from "./common_test.ts";
 import { assertRejects, restore } from "./dev_deps.ts";
 import { assertEquals, beforeEach, describe, it } from "./dev_deps.ts";
 import { GeoAPIClient } from "./geo_api_client.ts";
+import { logger, LogSeverity } from "./logging.ts";
+
+logger.setUpLogger(
+  "development",
+  LogSeverity.EMERGENCY,
+);
 
 const testEndpointUrl = "https://endpoint.geo";
 const testPlaceId = "place123";
diff --git a/app/server.ts b/app/server.ts
index ed0a47cf634d434dfea9f8f7b539faaae1d7a1b4..931c291485ee8a69ab5345eb93f9086a9109803e 100644
--- a/app/server.ts
+++ b/app/server.ts
@@ -2,7 +2,8 @@ import {
   CategoriesResponse,
   EngagementOpportunitiesParameters,
   EngagementParameters,
-  EngagementRecommendationsParameters,
+  EngagementRecosParameters,
+  EngagementRecosResponse,
   EngagementResponse,
   EngagementsResponse,
   FilteredEngagementsParameters,
@@ -53,17 +54,17 @@ const typeDefs = `
       """
       If this engagement is part of a recommendation query response, matchedTopics contains the slugs of matched topics. The order is defined as follows: 1) topics that were part of the query come first (sorted by matching confidence). 2) other topics the engagement matches (sorted by matching confidence)
       """
-      matchedTopics: [String]
+      matchedTopics: [String] @deprecated(reason: "Use the identically named field in the EngagementReco type returned by the engagementRecos query instead.")
       
       """
       If this engagement is part of a recommendation query response, matchedSkills contains the slugs of matched skills. The order is defined as follows: 1) skills that were part of the query come first (sorted by matching confidence). 2) other topics the engagement matches (sorted by matching confidence)
       """
-      matchedSkills: [String]
+      matchedSkills: [String] @deprecated(reason: "Use the identically named field in the EngagementReco type returned by the engagementRecos query instead.")
       
       """
       If this engagement is part of a recommendation query response, matchedGeoLocationDistanceInMeters contains the distance (in meters) between the engagement and the location given in the query.
       """
-      matchedGeoLocationDistanceInMeters: Float
+      matchedGeoLocationDistanceInMeters: Float @deprecated(reason: "Use the identically named field in the EngagementReco type returned by the engagementRecos query instead.")
     }
 
     type Category {
@@ -79,6 +80,31 @@ const typeDefs = `
       data: [Engagement]!
     }
     
+    type EngagementReco {
+    
+      engagement: Engagement!
+      
+      """
+      Contains the slugs of matched topics. The order is defined as follows: 1) topics that were part of the query come first (sorted by matching confidence). 2) other topics the engagement matches (sorted by matching confidence)
+      """
+      matchedTopics: [String]
+      
+      """
+      Contains the slugs of matched skills. The order is defined as follows: 1) skills that were part of the query come first (sorted by matching confidence). 2) other topics the engagement matches (sorted by matching confidence)
+      """
+      matchedSkills: [String]
+      
+      """
+      Contains the distance (in meters) between the engagement and the location given in the query.
+      """
+      matchedGeoLocationDistanceInMeters: Float
+    }
+    
+    type EngagementRecosResponse {
+      totalResults: Int!
+      data: [EngagementReco]!
+    }
+    
     scalar GeoJSON
 
     type Query {
@@ -88,11 +114,17 @@ const typeDefs = `
         """
         Retrieves a list of recommendations based on the request parameters "topics", "skills" and "geoLocationId". "topics" contains slugs of the "Topic" type, "skills" contains slugs of the "Skill" type, "geoLocationId" contains the ID of a location as returned by the "geo_*" queries.
         """
-        engagementRecommendations(offset: Int! = 0, limit: Int! = 10, topics: [String!] = [], skills: [String!] = [], geolocationId: String): EngagementsResponse!
+        engagementRecommendations(offset: Int! = 0, limit: Int! = 10, topics: [String!] = [], skills: [String!] = [], geolocationId: String): EngagementsResponse! @deprecated(reason: "Use the engagementRecos query instead.")
+        
+        """
+        Retrieves a list of recommendations based on the request parameters "topics", "skills" and "geoLocationId". "topics" contains slugs of the "Topic" type, "skills" contains slugs of the "Skill" type, "geoLocationId" contains the ID of a location as returned by the "geo_*" queries.
+        """
+        engagementRecos(offset: Int! = 0, limit: Int! = 10, topics: [String!] = [], skills: [String!] = [], geolocationId: String): EngagementRecosResponse!
         
         engagement(id: String!): Engagement
         
         categories: CategoriesResponse!
+        
         filteredEngagements(offset: Int! = 0, limit: Int! = 10, topics: [String!] = [], skills: [String!] = [], geolocationId: String): EngagementsResponse!
     }
 
@@ -130,9 +162,15 @@ const createResolvers = (
     engagementRecommendations: (
       // deno-lint-ignore no-explicit-any
       _parent: any,
-      parameters: EngagementRecommendationsParameters,
+      parameters: EngagementRecosParameters,
     ): Promise<EngagementsResponse> =>
       config.fake ? Promise.resolve({ totalResults: 0, data: [] }) : volunteeringDB.findRecommendations(parameters),
+    engagementRecos: (
+      // deno-lint-ignore no-explicit-any
+      _parent: any,
+      parameters: EngagementRecosParameters,
+    ): Promise<EngagementRecosResponse> =>
+      config.fake ? Promise.resolve({ totalResults: 0, data: [] }) : volunteeringDB.findRecos(parameters),
   },
   Mutation: {
     trackEngagementView: (
@@ -222,7 +260,7 @@ export const startServer = (config: ServerConfig): Deno.HttpServer<Deno.NetAddr>
       debug: {
         queries: false,
         notices: false,
-        results: false,
+        results: true,
         queryInError: true,
       },
     },
diff --git a/app/types.ts b/app/types.ts
index f41a44d2bd8df33bf714aa4c386369e4ade74ba1..f2c5abdaa8e636ef7d574591afa8086c8dceb161 100644
--- a/app/types.ts
+++ b/app/types.ts
@@ -39,6 +39,19 @@ export type EngagementsResponse = {
   data: Engagement[];
 };
 
+export type EngagementReco = {
+  engagement: Engagement;
+
+  matchedTopics?: string[];
+  matchedSkills?: string[];
+  matchedGeoLocationDistanceInMeters?: number;
+};
+
+export type EngagementRecosResponse = {
+  totalResults: number;
+  data: EngagementReco[];
+};
+
 export type EngagementOpportunitiesParameters = {
   limit: number;
   offset: number;
@@ -60,7 +73,7 @@ export type TrackEngagementViewResponse = {
   id: string;
 };
 
-export type EngagementRecommendationsParameters = {
+export type EngagementRecosParameters = {
   limit: number;
   offset: number;
   skills: string[];
diff --git a/app/voltastics_test.ts b/app/voltastics_test.ts
index 4ee4ae5f80b04e1cab201ed0a0a5ada7a035e664..1ec4bc77d127a693e0486d3260bba50b5fc1f407 100644
--- a/app/voltastics_test.ts
+++ b/app/voltastics_test.ts
@@ -14,7 +14,8 @@ import { processGqlRequest, stubFetch } from "./common_test.ts";
 import {
   CategoriesResponse,
   EngagementParameters,
-  EngagementRecommendationsParameters,
+  EngagementRecosParameters,
+  EngagementRecosResponse,
   EngagementResponse,
   EngagementsResponse,
   FilteredEngagementsParameters,
@@ -30,6 +31,12 @@ import {
 } from "./voltastics_test_data.ts";
 import { fetchCategories, fetchEngagementOpportunities } from "./voltastics.ts";
 import { VolunteeringDB } from "./volunteering_db.ts";
+import { logger, LogSeverity } from "./logging.ts";
+
+logger.setUpLogger(
+  "development",
+  LogSeverity.EMERGENCY,
+);
 
 const emptyResponse = {
   success: true,
@@ -147,7 +154,10 @@ const fakeVolunteeringDB = new (class implements VolunteeringDB {
   public engagementById(_params: EngagementParameters): Promise<EngagementResponse> {
     return Promise.reject(new Error("should not have been called"));
   }
-  findRecommendations(_params: EngagementRecommendationsParameters): Promise<EngagementsResponse> {
+  findRecommendations(_params: EngagementRecosParameters): Promise<EngagementsResponse> {
+    return Promise.reject(new Error("should not have been called"));
+  }
+  findRecos(_params: EngagementRecosParameters): Promise<EngagementRecosResponse> {
     return Promise.reject(new Error("should not have been called"));
   }
   filterEngagements(_params: FilteredEngagementsParameters): Promise<EngagementsResponse> {
diff --git a/app/volunteering_db.ts b/app/volunteering_db.ts
index 0544f68e242ef515f942a7f4a0ac0aea60c5db1b..b52a0a67f870aa2beea346aa867a25e6d3703d7a 100644
--- a/app/volunteering_db.ts
+++ b/app/volunteering_db.ts
@@ -4,7 +4,9 @@ import { GeoAPIClient } from "./geo_api_client.ts";
 import {
   Engagement,
   EngagementParameters,
-  EngagementRecommendationsParameters,
+  EngagementReco,
+  EngagementRecosParameters,
+  EngagementRecosResponse,
   EngagementResponse,
   EngagementsResponse,
   FilteredEngagementsParameters,
@@ -238,21 +240,40 @@ export const exportedForTesting = {
   SKILLS_VECTOR_INDICES,
 };
 
-const toEngagement = (
+const addQueryMatchInfo = (
+  row: VolunteeringDBRow,
+  engagement: Engagement,
+  queriedTopics: string[],
+  queriedSkills: string[],
+) => {
+  const embedding = JSON.parse(row.embedding_array) as number[];
+  const { topics, skills } = decodeMatchesFromEmbedding(embedding, queriedTopics, queriedSkills);
+  return {
+    ...engagement,
+    matchedTopics: topics,
+    matchedSkills: skills,
+    matchedGeoLocationDistanceInMeters: row.distance_in_meters,
+  };
+};
+
+const toEngagementRecommendation = (
   imageProxyBaseUrl: string,
   row: VolunteeringDBRow,
-  options: {
-    addQueryMatchInfo: false;
-  } | {
-    addQueryMatchInfo: true;
-    queriedTopics: string[];
-    queriedSkills: string[];
-  },
-): Engagement => {
+  queriedTopics: string[],
+  queriedSkills: string[],
+): EngagementReco => {
+  const embedding = JSON.parse(row.embedding_array) as number[];
+  const { topics, skills } = decodeMatchesFromEmbedding(embedding, queriedTopics, queriedSkills);
+  return {
+    engagement: toEngagement(imageProxyBaseUrl, row),
+    matchedTopics: topics,
+    matchedSkills: skills,
+    matchedGeoLocationDistanceInMeters: row.distance_in_meters,
+  };
+};
+
+const toEngagement = (imageProxyBaseUrl: string, row: VolunteeringDBRow): Engagement => {
   const embedding = JSON.parse(row.embedding_array) as number[];
-  const { topics: matchedTopics, skills: matchedSkills } = options.addQueryMatchInfo
-    ? decodeMatchesFromEmbedding(embedding, options.queriedTopics, options.queriedSkills)
-    : { topics: undefined, skills: undefined };
   const { topics, skills } = decodeMatchesFromEmbedding(embedding, [], []);
   return {
     id: "" + row.id,
@@ -279,9 +300,6 @@ const toEngagement = (
     longitude: row.longitude,
     topics: topics,
     skills: skills,
-    matchedTopics,
-    matchedSkills,
-    matchedGeoLocationDistanceInMeters: row.distance_in_meters,
   };
 };
 
@@ -295,7 +313,8 @@ const ERROR_CODE_NOT_FOUND = "NOT_FOUND";
 
 export interface VolunteeringDB {
   engagementById(params: EngagementParameters): Promise<EngagementResponse>;
-  findRecommendations(params: EngagementRecommendationsParameters): Promise<EngagementsResponse>;
+  findRecommendations(params: EngagementRecosParameters): Promise<EngagementsResponse>;
+  findRecos(params: EngagementRecosParameters): Promise<EngagementRecosResponse>;
   filterEngagements(params: FilteredEngagementsParameters): Promise<EngagementsResponse>;
 }
 
@@ -329,12 +348,12 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
       );
     } else {
       const row = result.rows[0];
-      return toEngagement(this.imageProxyBaseUrl, row, { addQueryMatchInfo: false });
+      return toEngagement(this.imageProxyBaseUrl, row);
     }
   }
 
   async findRecommendations(
-    { offset, limit, topics, skills, geolocationId }: EngagementRecommendationsParameters,
+    { offset, limit, topics, skills, geolocationId }: EngagementRecosParameters,
   ): Promise<EngagementsResponse> {
     const result = await this.queryRecos(
       offset < 0 ? 0 : offset,
@@ -347,11 +366,7 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
       throw reason;
     });
     const recos = result.rows.map((row) =>
-      toEngagement(this.imageProxyBaseUrl, row, {
-        addQueryMatchInfo: true,
-        queriedTopics: topics,
-        queriedSkills: skills,
-      })
+      addQueryMatchInfo(row, toEngagement(this.imageProxyBaseUrl, row), topics, skills)
     );
     return Promise.resolve({
       totalResults: extractTotalResultCount(result),
@@ -359,6 +374,26 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     });
   }
 
+  async findRecos(
+    { offset, limit, topics, skills, geolocationId }: EngagementRecosParameters,
+  ): Promise<EngagementRecosResponse> {
+    const result = await this.queryRecos(
+      offset < 0 ? 0 : offset,
+      limit > MAX_LIMIT || limit < 1 ? DEFAULT_LIMIT : limit,
+      topics,
+      skills,
+      geolocationId,
+    ).catch((reason) => {
+      logger.error("Error retrieving recommendations. Reason: ", reason);
+      throw reason;
+    });
+    const recos = result.rows.map((row) => toEngagementRecommendation(this.imageProxyBaseUrl, row, topics, skills));
+    return Promise.resolve({
+      totalResults: extractTotalResultCount(result),
+      data: recos,
+    });
+  }
+
   private async queryRecos(
     offset: number,
     limit: number,
@@ -606,7 +641,7 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
 
     return {
       totalResults,
-      data: result.rows.map((row) => toEngagement(this.imageProxyBaseUrl, row, { addQueryMatchInfo: false })),
+      data: result.rows.map((row) => toEngagement(this.imageProxyBaseUrl, row)),
     };
   }
 }
diff --git a/app/volunteering_db_test.ts b/app/volunteering_db_test.ts
index 7aad07da1b1f08249076594dc7f5d273c1de05a2..a7bdc079dac127ea243f6e3fd5df67dbd67aa320 100644
--- a/app/volunteering_db_test.ts
+++ b/app/volunteering_db_test.ts
@@ -12,6 +12,13 @@ import {
 import { exportedForTesting, PostgresVolunteeringDB, VolunteeringDB, VolunteeringDBRow } from "./volunteering_db.ts";
 import { Client, GraphQLError } from "./deps.ts";
 import { GeoAPIClient } from "./geo_api_client.ts";
+import { Engagement } from "./types.ts";
+import { logger, LogSeverity } from "./logging.ts";
+
+logger.setUpLogger(
+  "development",
+  LogSeverity.EMERGENCY,
+);
 
 // taken from https://stackoverflow.com/a/2450976 ("Fisher-Yates Shuffle")
 function shuffle<A>(array: A[]): A[] {
@@ -66,6 +73,75 @@ const withMockedDependencies = (queryObjectResult: VolunteeringDBRow[] = []) =>
   test(volunteeringDB, mockedGeoApiClient);
 };
 
+const embeddingFixtures = {
+  // deno-fmt-ignore
+  embedding_array: [
+    1,0,0,0,0,0,0,0,0,1,
+    0,0,0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,1,
+    1,0,0,0,0,0,0,0,0,1,
+    0,0,0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,0,0,0,
+    0,0,0,0,0,0,0,0,0,1,
+  ],
+};
+
+const dbFixtures = {
+  row1: {
+    id: 1,
+    title: "moin",
+    description: "moin blabla",
+    link: "https://moin.bla",
+    image: "https://moin.bla/blub.jpg",
+    source: "moin_bla",
+    uses: "blib",
+    orga: "blob",
+    imageorga: "https://moin.bla/blob.jpg",
+    location: "Blibhausen",
+    latitude: 12.3456,
+    longitude: 65.4321,
+    embedding_array: JSON.stringify(embeddingFixtures.embedding_array),
+    cosine_similarity: 0,
+    distance_in_meters: 0,
+    total_results: 1,
+  } as VolunteeringDBRow,
+};
+
+const assertEngagementMatchesRow = (row: VolunteeringDBRow, engagement: Engagement) => {
+  assertEquals(engagement.id, `${row.id}`);
+  assertEquals(engagement.title, row.title);
+  assertEquals(engagement.description, row.description);
+  assertEquals(engagement.url, row.link);
+  assertEquals(
+    engagement.imageUrl,
+    "mock-image-proxy-base-url/crop:0:0/resize:auto:1024/plain/" + encodeURIComponent(row.image!),
+  );
+  assertEquals(engagement.imageUrlOriginal, row.image);
+  assertEquals(engagement.source, row.source);
+  assertEquals(engagement.categories, undefined);
+  assertEquals(engagement.organizer, {
+    name: row.orga,
+    imageUrl: row.imageorga,
+  });
+  assertEquals(engagement.location, row.location);
+  assertEquals(engagement.latitude, row.latitude);
+  assertEquals(engagement.longitude, row.longitude);
+  assertEquals(engagement.topics, [
+    "agriculture-food", // first item (index 0)
+    "disabilities-inclusion", // tenth item (index 9)
+    "water-ocean", // last item  (index 27)
+  ]);
+  assertEquals(engagement.skills, [
+    "adaptability", // first item (index 0)
+    "conflict-management", // tenth item (index 9)
+    "writing-translation", // last item  (index 49)
+  ]);
+  assertEquals(engagement.matchedTopics, undefined);
+  assertEquals(engagement.matchedSkills, undefined);
+  assertEquals(engagement.matchedGeoLocationDistanceInMeters, undefined);
+};
+
 describe("VolunteeringDB", () => {
   describe("queryEmbeddingVector", () => {
     it("correctly constructs a query embedding vector", () => {
@@ -228,68 +304,9 @@ describe("VolunteeringDB", () => {
 
   describe("querying engagement by id", () => {
     it("correctly parses engagement", () => {
-      // deno-fmt-ignore
-      const embedding_array = [
-        1,0,0,0,0,0,0,0,0,1,
-        0,0,0,0,0,0,0,0,0,0,
-        0,0,0,0,0,0,0,1,
-        1,0,0,0,0,0,0,0,0,1,
-        0,0,0,0,0,0,0,0,0,0,
-        0,0,0,0,0,0,0,0,0,0,
-        0,0,0,0,0,0,0,0,0,0,
-        0,0,0,0,0,0,0,0,0,1,
-      ]
-      const dbRow: VolunteeringDBRow = {
-        id: 1,
-        title: "moin",
-        description: "moin blabla",
-        link: "https://moin.bla",
-        image: "https://moin.bla/blub.jpg",
-        source: "moin_bla",
-        uses: "blib",
-        orga: "blob",
-        imageorga: "https://moin.bla/blob.jpg",
-        location: "Blibhausen",
-        latitude: 12.3456,
-        longitude: 65.4321,
-        embedding_array: JSON.stringify(embedding_array),
-        cosine_similarity: 0,
-        distance_in_meters: 0,
-        total_results: 1,
-      };
-      withMockedDependencies([dbRow])(async (volunteeringDB) => {
+      withMockedDependencies([dbFixtures.row1])(async (volunteeringDB) => {
         const response = await volunteeringDB.engagementById({ id: "1" });
-        assertEquals(response?.id, `${dbRow.id}`);
-        assertEquals(response?.title, dbRow.title);
-        assertEquals(response?.description, dbRow.description);
-        assertEquals(response?.url, dbRow.link);
-        assertEquals(
-          response?.imageUrl,
-          "mock-image-proxy-base-url/crop:0:0/resize:auto:1024/plain/" + encodeURIComponent(dbRow.image!),
-        );
-        assertEquals(response?.imageUrlOriginal, dbRow.image);
-        assertEquals(response?.source, dbRow.source);
-        assertEquals(response?.categories, undefined);
-        assertEquals(response?.organizer, {
-          name: dbRow.orga,
-          imageUrl: dbRow.imageorga,
-        });
-        assertEquals(response?.location, dbRow.location);
-        assertEquals(response?.latitude, dbRow.latitude);
-        assertEquals(response?.longitude, dbRow.longitude);
-        assertEquals(response?.topics, [
-          "agriculture-food", // first item (index 0)
-          "disabilities-inclusion", // tenth item (index 9)
-          "water-ocean", // last item  (index 27)
-        ]);
-        assertEquals(response?.skills, [
-          "adaptability", // first item (index 0)
-          "conflict-management", // tenth item (index 9)
-          "writing-translation", // last item  (index 49)
-        ]);
-        assertEquals(response?.matchedTopics, undefined);
-        assertEquals(response?.matchedSkills, undefined);
-        assertEquals(response?.matchedGeoLocationDistanceInMeters, 0);
+        assertEngagementMatchesRow(dbFixtures.row1, response!);
       });
     });
 
@@ -300,7 +317,7 @@ describe("VolunteeringDB", () => {
       });
     });
   });
-
+  // TODO findRecommendations is deprecated, can be removed after clients are migrated to findRecommendationsV2
   describe("findRecommendations", () => {
     it("resolves latitude/longitude using the GeoAPI client when geolocationId is given", () => {
       withMockedDependencies()(
@@ -376,6 +393,109 @@ describe("VolunteeringDB", () => {
       );
     });
   });
+  describe("findRecos", () => {
+    it("wraps search results with matchedTopics, matchedSkills and matchedGeoLocationDistanceInMeters", () => {
+      withMockedDependencies([dbFixtures.row1])(async (volunteeringDB, _geoApiClient) => {
+        const response = await volunteeringDB.findRecos({
+          limit: 10,
+          offset: 0,
+          topics: ["disabilities-inclusion", "water-ocean"],
+          skills: ["adaptability", "writing-translation"],
+          geolocationId: "mock-geolocation-id",
+        });
+        assertEquals(response.totalResults, 1);
+        assertEquals(response.data.length, 1);
+
+        const reco = response.data[0];
+        assertEngagementMatchesRow(dbFixtures.row1, reco.engagement);
+
+        assertEquals(reco.matchedTopics, [
+          "disabilities-inclusion", // tenth item (index 9)
+          "water-ocean", // last item  (index 27)
+          "agriculture-food", // first item (index 0)
+        ]);
+        assertEquals(reco.matchedSkills, [
+          "adaptability", // first item (index 0)
+          "writing-translation", // last item  (index 49)
+          "conflict-management", // tenth item (index 9)
+        ]);
+        assertEquals(reco.matchedGeoLocationDistanceInMeters, 0);
+      });
+    });
+    it("resolves latitude/longitude using the GeoAPI client when geolocationId is given", () => {
+      withMockedDependencies()(
+        (volunteeringDB, geoAPIClient) => {
+          volunteeringDB.findRecos({
+            limit: 10,
+            offset: 0,
+            topics: [],
+            skills: [],
+            geolocationId: "mock-geolocation-id",
+          });
+          assertSpyCall(geoAPIClient.resolveCoordinates as Stub, 0, {
+            args: ["mock-geolocation-id"],
+          });
+        },
+      );
+    });
+    it("does not resolve latitude/longitude when geolocationId is not given", () => {
+      withMockedDependencies()(
+        (volunteeringDB, geoAPIClient) => {
+          volunteeringDB.findRecos({
+            limit: 10,
+            offset: 0,
+            topics: ["agriculture-food"],
+            skills: [],
+          });
+          assertSpyCalls(geoAPIClient.resolveCoordinates as Stub, 0);
+        },
+      );
+    });
+    it("falls back to recommend based only on topics and skills if given and geo location resolution fails", () => {
+      withMockedDependencies()(
+        async (volunteeringDB, geoAPIClient) => {
+          stub(
+            geoAPIClient,
+            "resolveCoordinates",
+            returnsNext([Promise.reject("boom!")]),
+          );
+
+          const result = await volunteeringDB.findRecos({
+            limit: 10,
+            offset: 0,
+            topics: ["agriculture-food"],
+            skills: [],
+            geolocationId: "mock-geolocation-id",
+          });
+          assertEquals(result.totalResults, 0);
+          assertEquals(result.data, []);
+        },
+      );
+    });
+    it("fails with error message if no topics and skills are given and geo location resolution fails", () => {
+      withMockedDependencies()(
+        async (volunteeringDB, geoAPIClient) => {
+          stub(
+            geoAPIClient,
+            "resolveCoordinates",
+            returnsNext([Promise.reject("boom!")]),
+          );
+          await assertRejects(
+            () =>
+              volunteeringDB.findRecos({
+                limit: 10,
+                offset: 0,
+                topics: [],
+                skills: [],
+                geolocationId: "mock-geolocation-id",
+              }),
+            Error,
+            "geo location resolution failed and no topics or skills given",
+          );
+        },
+      );
+    });
+  });
 
   describe("filterEngagements", () => {
     it("resolves geolocation geometry using the GeoAPI client when geolocationId is given", () => {
diff --git a/deno.lock b/deno.lock
index fa5e40111d6cdfe2a7f08d05f0f0e6f559be70ae..0edcbcb380b38efcb3f1e4ca75903a76b8e8be39 100644
--- a/deno.lock
+++ b/deno.lock
@@ -477,6 +477,7 @@
     "https://deno.land/std@0.156.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239",
     "https://deno.land/std@0.156.0/async/tee.ts": "d27680d911816fcb3d231e16d690e7588079e66a9b2e5ce8cc354db94fdce95f",
     "https://deno.land/std@0.156.0/http/server.ts": "c1bce1cbf4060055f622d5c3f0e406fd553e5dca111ca836d28c6268f170ebeb",
+    "https://deno.land/std@0.170.0/encoding/base64.ts": "8605e018e49211efc767686f6f687827d7f5fd5217163e981d8d693105640d7a",
     "https://deno.land/std@0.214.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5",
     "https://deno.land/std@0.214.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8",
     "https://deno.land/std@0.214.0/async/delay.ts": "8e1d18fe8b28ff95885e2bc54eccec1713f57f756053576d8228e6ca110793ad",
@@ -589,6 +590,8 @@
     "https://deno.land/std@0.224.0/async/pool.ts": "2b972e3643444b73f6a8bcdd19799a2d0821b28a45fbe47fd333223eb84327f0",
     "https://deno.land/std@0.224.0/async/retry.ts": "29025b09259c22123c599b8c957aeff2755854272954776dc9a5846c72ea4cfe",
     "https://deno.land/std@0.224.0/async/tee.ts": "34373c58950b7ac5950632dc8c9908076abeefcc9032d6299fff92194c284fbd",
+    "https://deno.land/x/cliffy@v0.25.7/ansi/ansi_escapes.ts": "885f61f343223f27b8ec69cc138a54bea30542924eacd0f290cd84edcf691387",
+    "https://deno.land/x/cliffy@v0.25.7/ansi/deps.ts": "0f35cb7e91868ce81561f6a77426ea8bc55dc15e13f84c7352f211023af79053",
     "https://deno.land/x/postgres@v0.19.3/client.ts": "d141c65c20484c545a1119c9af7a52dcc24f75c1a5633de2b9617b0f4b2ed5c1",
     "https://deno.land/x/postgres@v0.19.3/client/error.ts": "05b0e35d65caf0ba21f7f6fab28c0811da83cd8b4897995a2f411c2c83391036",
     "https://deno.land/x/postgres@v0.19.3/connection/auth.ts": "db15c1659742ef4d2791b32834950278dc7a40cb931f8e434e6569298e58df51",