diff --git a/.envrc.local.template b/.envrc.local.template
index b175c0221ee15e15641b3b36b31cb1f161a60e50..7ecaf8843225d330188d3942945f6145be190b8f 100644
--- a/.envrc.local.template
+++ b/.envrc.local.template
@@ -1,3 +1,4 @@
+export SENTRY_DSN=
 export CACHE_ENABLED=false
 
 export VOLUNTEERING_VOLTASTICS_API_URL=
diff --git a/README.md b/README.md
index c63f69319b80243bc220bf0f733a2c9f905ad998..f99d23ca2917a3618a8aa31b16451755101eb5a4 100644
--- a/README.md
+++ b/README.md
@@ -193,13 +193,19 @@ if "you know what you're doing".
 
 ### Configuration
 
-| Environment Variable            | Default Value                                                                                | Description                                             |
-| ------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
-| PORT                            | 8004                                                                                         | the port to listen on                                   |
-| CACHE_ENABLED                   | true                                                                                         | whether or not to enable caching                        |
-| CACHE_TTL_MS_VOLTASTICS         | 24 hours                                                                                     | time-to-live in ms for data fetched from Voltastics API |
-| CACHE_TTL_MS_DB                 | 1 hour                                                                                       | time-to-live in ms for data fetched from DB             |
-| VOLUNTEERING_VOLTASTICS_API_URL | undefined                                                                                    | Voltastics API base URL                                 |
-| VOLUNTEERING_VOLTASTICS_API_KEY | undefined                                                                                    | Voltastics API Token                                    |
-| IMAGE_PROXY_BASE_URL            | <https://images.holi.social> (production), <https://dev-images.holi.social> (all other envs) | Base URL for the image proxy server                     |
-| GEO_API_ENDPOINT_URL            | undefined                                                                                    | GraphQL Endpoint URL for holis Geo API                  |
+| Environment Variable            | Default Value                                                                                | Description                                                              |
+|---------------------------------|----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
+| PORT                            | 8004                                                                                         | the port to listen on                                                    |
+| CACHE_ENABLED                   | true                                                                                         | whether or not to enable caching                                         |
+| CACHE_TTL_MS_VOLTASTICS         | 24 hours                                                                                     | time-to-live in ms for data fetched from Voltastics API                  |
+| CACHE_TTL_MS_DB                 | 1 hour                                                                                       | time-to-live in ms for data fetched from DB                              |
+| VOLUNTEERING_VOLTASTICS_API_URL | undefined                                                                                    | Voltastics API base URL                                                  |
+| VOLUNTEERING_VOLTASTICS_API_KEY | undefined                                                                                    | Voltastics API Token                                                     |
+| IMAGE_PROXY_BASE_URL            | <https://images.holi.social> (production), <https://dev-images.holi.social> (all other envs) | Base URL for the image proxy server                                      |
+| GEO_API_ENDPOINT_URL            | undefined                                                                                    | GraphQL Endpoint URL for holis Geo API                                   |
+| DB_HOST                         | undefined                                                                                    | Recommendations DB hostname                                              |
+| DB_NAME                         | undefined                                                                                    | Recommendations DB database name                                         |
+| DB_USERNAME                     | undefined                                                                                    | Recommendations DB username                                              |
+| DB_PASSWORD                     | undefined                                                                                    | Recommendations DB password                                              |
+| DB_CONNECTION_ATTEMPTS          | 10                                                                                           | Recommendations DB - how often to attempt a reconnect on connection loss |
+| SENTRY_DSN                      | undefined                                                                                    | The Sentry DSN to sent traces to, disabled if not given                  |
diff --git a/app/config.ts b/app/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..88f2c07ab56763f7f803bb3bcd2fdb83b229fbb9
--- /dev/null
+++ b/app/config.ts
@@ -0,0 +1,116 @@
+import { VolunteeringDBConfig } from "./volunteering_db.ts";
+
+export interface VoltasticsConfig {
+  baseUrl: string;
+  apiToken: string;
+}
+
+export interface ServerConfig {
+  port: number; // default: 8004
+  cacheEnabled: boolean; // default: true
+  cacheTtlMsVoltastics: number; // default: 24 hours
+  cacheTtlMsDB: number; // default: 1 hour
+  voltastics: VoltasticsConfig;
+  imageProxyBaseUrl: string;
+  fake: boolean; // For local development. If set, the API returns dummy data
+  volunteeringDB: VolunteeringDBConfig;
+  geoAPIEndpointUrl: string;
+}
+
+export const requiredEnv = <T>(
+  name: string,
+  typeFn: (s: string) => T,
+  fallback?: T,
+): T => {
+  const env = Deno.env.get(name);
+  if (env === undefined && fallback === undefined) {
+    throw Error(`Environment variable "${name}" is required`);
+  } else {
+    return env !== undefined ? typeFn(env) : fallback!;
+  }
+};
+
+const asBoolean = (str: string) => /^true$/i.test(str);
+
+const fake = requiredEnv("FAKE", asBoolean, false); // For local development. If set, the API returns dummy data
+
+export const environment = Deno.env.get("ENVIRONMENT")?.toLowerCase().trim() || "development";
+
+const DEFAULT_PORT = 8004;
+const DEFAULT_CACHE_ENABLED = true;
+const DEFAULT_CACHE_TTL_MS_VOLTASTICS = 86_400_000; // 24 hours
+const DEFAULT_CACHE_TTL_MS_DB = 3_600_000; // 1 hour
+
+const DEFAULT_DB_CONNECTION_ATTEMPTS = 10;
+
+export const serverConfigFromEnv = (): ServerConfig => {
+  return {
+    port: requiredEnv("PORT", Number, DEFAULT_PORT),
+    cacheEnabled: requiredEnv(
+      "CACHE_ENABLED",
+      asBoolean,
+      DEFAULT_CACHE_ENABLED,
+    ),
+    cacheTtlMsVoltastics: requiredEnv(
+      "DEFAULT_CACHE_TTL_MS_VOLTASTICS",
+      Number,
+      DEFAULT_CACHE_TTL_MS_VOLTASTICS,
+    ),
+    cacheTtlMsDB: requiredEnv(
+      "DEFAULT_CACHE_TTL_MS_DB",
+      Number,
+      DEFAULT_CACHE_TTL_MS_DB,
+    ),
+    voltastics: {
+      baseUrl: requiredEnv(
+        "VOLUNTEERING_VOLTASTICS_API_URL",
+        String,
+        fake ? "dummy value" : undefined,
+      ),
+      apiToken: requiredEnv(
+        "VOLUNTEERING_VOLTASTICS_API_KEY",
+        String,
+        fake ? "dummy value" : undefined,
+      ),
+    },
+    imageProxyBaseUrl: requiredEnv(
+      "IMAGE_PROXY_BASE_URL",
+      String,
+      environment === "production" ? "https://images.holi.social" : "https://dev-images.holi.social",
+    ),
+    volunteeringDB: {
+      hostname: requiredEnv(
+        "DB_HOST",
+        String,
+        fake ? "dummy value" : undefined,
+      ),
+      port: requiredEnv("DB_PORT", String, fake ? "31337" : undefined),
+      database: requiredEnv(
+        "DB_NAME",
+        String,
+        fake ? "dummy value" : undefined,
+      ),
+      username: requiredEnv(
+        "DB_USERNAME",
+        String,
+        fake ? "dummy value" : undefined,
+      ),
+      password: requiredEnv(
+        "DB_PASSWORD",
+        String,
+        fake ? "dummy value" : undefined,
+      ),
+      connectionAttempts: requiredEnv(
+        "DB_CONNECTION_ATTEMPTS",
+        Number,
+        DEFAULT_DB_CONNECTION_ATTEMPTS,
+      ),
+    },
+    fake,
+    geoAPIEndpointUrl: requiredEnv(
+      "GEO_API_ENDPOINT_URL",
+      String,
+      fake ? "dummy value" : undefined,
+    ),
+  };
+};
diff --git a/app/deps.ts b/app/deps.ts
index 339c7690bc7ff1a0be5909311f52c0ab38397344..2b9a05d03cc3f0dada7879626490391409fed8cd 100644
--- a/app/deps.ts
+++ b/app/deps.ts
@@ -4,3 +4,4 @@ export { GraphQLError } from "npm:graphql@16.8.1";
 export * as turf from "https://esm.sh/@turf/turf@6.5.0";
 export { Client, type QueryObjectResult } from "https://deno.land/x/postgres/mod.ts";
 export { deadline, DeadlineError } from "https://deno.land/std/async/mod.ts";
+export * as Sentry from "https://deno.land/x/sentry/index.mjs";
diff --git a/app/geo_api_client.ts b/app/geo_api_client.ts
index b847481a322e423777f943b64e361ed0bf5edc1d..3089a4f7fd254a70b0603de95808e929bafab753 100644
--- a/app/geo_api_client.ts
+++ b/app/geo_api_client.ts
@@ -1,5 +1,6 @@
 import { GeolocationCoordinates, GeolocationGeometry, GeolocationProperties } from "./types.ts";
 import { logger } from "./logging.ts";
+import { Sentry } from "./deps.ts";
 
 type GeoAPIResponse = {
   data?: {
@@ -22,7 +23,12 @@ type GeoAPIAutocompleteResponse = {
   };
 };
 
-export class GeoAPIClient {
+export interface GeoAPIClient {
+  resolveCoordinates(geolocationId: string): Promise<GeolocationCoordinates>;
+  resolveGeometry(geolocationId: string, options?: { allowPoints?: boolean }): Promise<GeolocationGeometry>;
+}
+
+export class GeoAPIClientImpl implements GeoAPIClient {
   private readonly geoAPIEndpointUrl: string;
 
   constructor(geoAPIEndpointUrl: string) {
@@ -32,71 +38,76 @@ export class GeoAPIClient {
   private async resolveGeolocationViaGeoAPI(
     geolocationId: string,
   ): Promise<GeoAPIResponse> {
-    const graphQLQuery = `{ placeDetails(id: "${geolocationId}") { geolocation } }`;
-    const response = await fetch(this.geoAPIEndpointUrl, {
-      "body": JSON.stringify({ query: graphQLQuery }),
-      "headers": {
-        "Accept": "application/graphql-response+json, application/json",
-        "Content-Type": "application/json",
-      },
-      "method": "POST",
+    return await Sentry.startSpan({ name: "GeoAPIClient.resolveGeolocationViaGeoAPI" }, async () => {
+      const graphQLQuery = `{ placeDetails(id: "${geolocationId}") { geolocation } }`;
+      const response = await fetch(this.geoAPIEndpointUrl, {
+        "body": JSON.stringify({ query: graphQLQuery }),
+        "headers": {
+          "Accept": "application/graphql-response+json, application/json",
+          "Content-Type": "application/json",
+        },
+        "method": "POST",
+      });
+      return await response.json() as GeoAPIResponse;
     });
-    return response.json() as GeoAPIResponse;
   }
 
   private async resolvePlaceIdByName(name: string): Promise<string | undefined> {
-    const graphQLQuery = `{ placesAutocomplete(text: "${name}", limit: 1) { id } }`;
-    const response = await fetch(this.geoAPIEndpointUrl, {
-      "body": JSON.stringify({ query: graphQLQuery }),
-      "headers": {
-        "Accept": "application/graphql-response+json, application/json",
-        "Content-Type": "application/json",
-      },
-      "method": "POST",
+    return await Sentry.startSpan({ name: "GeoAPIClient.resolvePlaceIdByName" }, async () => {
+      const graphQLQuery = `{ placesAutocomplete(text: "${name}", limit: 1) { id } }`;
+      const response = await fetch(this.geoAPIEndpointUrl, {
+        "body": JSON.stringify({ query: graphQLQuery }),
+        "headers": {
+          "Accept": "application/graphql-response+json, application/json",
+          "Content-Type": "application/json",
+        },
+        "method": "POST",
+      });
+      const { data } = await response.json() as GeoAPIAutocompleteResponse;
+      const [place] = data?.placesAutocomplete || [];
+      return place?.id;
     });
-    const { data } = await response.json() as GeoAPIAutocompleteResponse;
-    const [place] = data?.placesAutocomplete || [];
-    return place?.id;
   }
 
   async resolveCoordinates(
     geolocationId: string,
   ): Promise<GeolocationCoordinates> {
-    const responseJSON = await this.resolveGeolocationViaGeoAPI(geolocationId);
-
-    const { lat, lon } = responseJSON.data?.placeDetails?.geolocation?.properties || {};
-    if (lat && lon) {
-      return { lat, lon };
-    } else {
-      return Promise.reject(
-        `Resolution of lat/lon failed (no data included in response for geolocationId=${geolocationId})`,
-      );
-    }
+    return await Sentry.startSpan({ name: "GeoAPIClient.resolveCoordinates" }, async () => {
+      const responseJSON = await this.resolveGeolocationViaGeoAPI(geolocationId);
+      const { lat, lon } = responseJSON.data?.placeDetails?.geolocation?.properties || {};
+      if (lat && lon) {
+        return { lat, lon };
+      } else {
+        return Promise.reject(
+          `Resolution of lat/lon failed (no data included in response for geolocationId=${geolocationId})`,
+        );
+      }
+    });
   }
 
   async resolveGeometry(
     geolocationId: string,
-    {
-      allowPoints = false,
-    }: { allowPoints?: boolean } = {},
+    options: { allowPoints?: boolean } = { allowPoints: false },
   ): Promise<GeolocationGeometry> {
-    const responseJSON = await this.resolveGeolocationViaGeoAPI(geolocationId);
-    const { geometry, properties } = responseJSON.data?.placeDetails?.geolocation || {};
-    const { type, coordinates } = geometry || {};
-    if (type && coordinates) {
-      const locationName = properties?.formatted;
-      if (type === "Point" && !allowPoints && locationName) {
-        logger.debug(`Retrieved point as geolocation, will retry with lookup for location ${locationName}`);
-        const placeId = await this.resolvePlaceIdByName(locationName);
-        if (placeId && placeId !== geolocationId) {
-          return this.resolveGeometry(placeId, { allowPoints: true });
+    return await Sentry.startSpan({ name: "GeoAPIClient.resolveGeometry" }, async () => {
+      const responseJSON = await this.resolveGeolocationViaGeoAPI(geolocationId);
+      const { geometry, properties } = responseJSON.data?.placeDetails?.geolocation || {};
+      const { type, coordinates } = geometry || {};
+      if (type && coordinates) {
+        const locationName = properties?.formatted;
+        if (type === "Point" && !options.allowPoints && locationName) {
+          logger.debug(`Retrieved point as geolocation, will retry with lookup for location ${locationName}`);
+          const placeId = await this.resolvePlaceIdByName(locationName);
+          if (placeId && placeId !== geolocationId) {
+            return this.resolveGeometry(placeId, { allowPoints: true });
+          }
         }
+        return { type, coordinates };
+      } else {
+        return Promise.reject(
+          `Resolution of geolocation geometry failed (no data included in response for geolocationId=${geolocationId})`,
+        );
       }
-      return { type, coordinates };
-    } else {
-      return Promise.reject(
-        `Resolution of geolocation geometry failed (no data included in response for geolocationId=${geolocationId})`,
-      );
-    }
+    });
   }
 }
diff --git a/app/geo_api_client_test.ts b/app/geo_api_client_test.ts
index 58b71f0ebd9bb61e077d78d1265a7e5a752468bf..e81fce8cfbb4342941933bdb67c1a151de7e5552 100644
--- a/app/geo_api_client_test.ts
+++ b/app/geo_api_client_test.ts
@@ -1,7 +1,7 @@
 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 { GeoAPIClientImpl } from "./geo_api_client.ts";
 import { logger, LogSeverity } from "./logging.ts";
 
 logger.setUpLogger(
@@ -59,11 +59,11 @@ const autocompleteResponse = {
 const invalidResponse = {};
 
 describe("GeoAPIClient", () => {
-  let client: GeoAPIClient;
+  let client: GeoAPIClientImpl;
 
   beforeEach(() => {
     restore();
-    client = new GeoAPIClient(testEndpointUrl);
+    client = new GeoAPIClientImpl(testEndpointUrl);
   });
 
   describe("resolveGeometry", () => {
diff --git a/app/main.ts b/app/main.ts
index de61bffe420f66ac83b35521a9e096562bd80881..bee5a70eb4276b211ee1b3b2a34b918cc93b4fd1 100644
--- a/app/main.ts
+++ b/app/main.ts
@@ -1,108 +1,24 @@
 import { logger, LogSeverity } from "./logging.ts";
-import {
-  DEFAULT_CACHE_ENABLED,
-  DEFAULT_CACHE_TTL_MS_DB,
-  DEFAULT_CACHE_TTL_MS_VOLTASTICS,
-  DEFAULT_DB_CONNECTION_ATTEMPTS,
-  DEFAULT_PORT,
-  ServerConfig,
-  startServer,
-} from "./server.ts";
-
-const environment = Deno.env.get("ENVIRONMENT") || "development";
+import { startServer } from "./server.ts";
+import { Sentry } from "./deps.ts";
+import { environment, serverConfigFromEnv } from "./config.ts";
 
 logger.setUpLogger(
   environment,
   environment === "development" ? LogSeverity.DEFAULT : LogSeverity.INFO,
 );
 
-const requiredEnv = <T>(
-  name: string,
-  typeFn: (s: string) => T,
-  fallback?: T,
-): T => {
-  const env = Deno.env.get(name);
-  if (env === undefined && fallback === undefined) {
-    throw Error(`Environment variable "${name}" is required`);
-  } else {
-    return env !== undefined ? typeFn(env) : fallback!;
-  }
-};
-
-const asBoolean = (str: string) => /^true$/i.test(str);
-
-const fake = requiredEnv("FAKE", asBoolean, false); // For local development. If set, the API returns dummy data
-const serverConfigFromEnv = (): ServerConfig => {
-  return {
-    port: requiredEnv("PORT", Number, DEFAULT_PORT),
-    cacheEnabled: requiredEnv(
-      "CACHE_ENABLED",
-      asBoolean,
-      DEFAULT_CACHE_ENABLED,
-    ),
-    cacheTtlMsVoltastics: requiredEnv(
-      "DEFAULT_CACHE_TTL_MS_VOLTASTICS",
-      Number,
-      DEFAULT_CACHE_TTL_MS_VOLTASTICS,
-    ),
-    cacheTtlMsDB: requiredEnv(
-      "DEFAULT_CACHE_TTL_MS_DB",
-      Number,
-      DEFAULT_CACHE_TTL_MS_DB,
-    ),
-    voltastics: {
-      baseUrl: requiredEnv(
-        "VOLUNTEERING_VOLTASTICS_API_URL",
-        String,
-        fake ? "dummy value" : undefined,
-      ),
-      apiToken: requiredEnv(
-        "VOLUNTEERING_VOLTASTICS_API_KEY",
-        String,
-        fake ? "dummy value" : undefined,
-      ),
-    },
-    imageProxyBaseUrl: requiredEnv(
-      "IMAGE_PROXY_BASE_URL",
-      String,
-      environment === "production" ? "https://images.holi.social" : "https://dev-images.holi.social",
-    ),
-    volunteeringDB: {
-      hostname: requiredEnv(
-        "DB_HOST",
-        String,
-        fake ? "dummy value" : undefined,
-      ),
-      port: requiredEnv("DB_PORT", String, fake ? "31337" : undefined),
-      database: requiredEnv(
-        "DB_NAME",
-        String,
-        fake ? "dummy value" : undefined,
-      ),
-      username: requiredEnv(
-        "DB_USERNAME",
-        String,
-        fake ? "dummy value" : undefined,
-      ),
-      password: requiredEnv(
-        "DB_PASSWORD",
-        String,
-        fake ? "dummy value" : undefined,
-      ),
-      connectionAttempts: requiredEnv(
-        "DB_CONNECTION_ATTEMPTS",
-        Number,
-        DEFAULT_DB_CONNECTION_ATTEMPTS,
-      ),
-    },
-    fake,
-    geoAPIEndpointUrl: requiredEnv(
-      "GEO_API_ENDPOINT_URL",
-      String,
-      fake ? "dummy value" : undefined,
-    ),
-  };
-};
+const sentryDSN = Deno.env.get("SENTRY_DSN");
+if (!sentryDSN) {
+  logger.warn('Environment variable "SENTRY_DSN" is not set, starting without Sentry integration...');
+} else {
+  const TRACING_SAMPLE_RATE = 0.25;
+  const TRACING_SAMPLE_RATE_DEVELOPMENT = 0.0;
+  Sentry.init({
+    dsn: sentryDSN,
+    tracesSampleRate: environment === "development" ? TRACING_SAMPLE_RATE_DEVELOPMENT : TRACING_SAMPLE_RATE,
+    environment,
+  });
+}
 
-const config = serverConfigFromEnv();
-await startServer(config);
+startServer(serverConfigFromEnv());
diff --git a/app/server.ts b/app/server.ts
index 931c291485ee8a69ab5345eb93f9086a9109803e..3ed03f750729af5048e26b409f312b6d7c00fa30 100644
--- a/app/server.ts
+++ b/app/server.ts
@@ -10,12 +10,12 @@ import {
   TrackEngagementViewParameters,
   TrackEngagementViewResponse,
 } from "./types.ts";
-import { createSchema, createYoga, deadline, DeadlineError, useResponseCache } from "./deps.ts";
+import { Client, createSchema, createYoga, deadline, DeadlineError, Sentry, useResponseCache } from "./deps.ts";
 import { fetchCategories, fetchEngagementOpportunities, trackEngagementView } from "./voltastics.ts";
 import { logger } from "./logging.ts";
-import { PostgresVolunteeringDB, VolunteeringDB, VolunteeringDBConfig } from "./volunteering_db.ts";
-import { GeoAPIClient } from "./geo_api_client.ts";
-import { Client } from "https://deno.land/x/postgres@v0.19.3/client.ts";
+import { PostgresVolunteeringDB, VolunteeringDB } from "./volunteering_db.ts";
+import { GeoAPIClientImpl } from "./geo_api_client.ts";
+import { ServerConfig } from "./config.ts";
 
 const typeDefs = `
     type Organizer {
@@ -144,33 +144,40 @@ const createResolvers = (
       parameters: EngagementOpportunitiesParameters,
     ): Promise<EngagementsResponse> =>
       config.fake ? Promise.resolve({ totalResults: 0, data: [] }) : fetchEngagementOpportunities(config)(parameters),
-    engagement: (
-      // deno-lint-ignore no-explicit-any
-      _parent: any,
-      parameters: EngagementParameters,
-    ): Promise<EngagementResponse> => config.fake ? Promise.resolve(null) : volunteeringDB.engagementById(parameters),
     categories: (
       // deno-lint-ignore no-explicit-any
       _parent: any,
     ): Promise<CategoriesResponse> => config.fake ? Promise.resolve({ data: [] }) : fetchCategories(config.voltastics),
+    engagement: (
+      // deno-lint-ignore no-explicit-any
+      _parent: any,
+      parameters: EngagementParameters,
+    ): Promise<EngagementResponse> =>
+      config.fake ? Promise.resolve(null) : Sentry.startNewTrace(() => volunteeringDB.engagement(parameters)),
     filteredEngagements: (
       // deno-lint-ignore no-explicit-any
       _parent: any,
       parameters: FilteredEngagementsParameters,
     ): Promise<EngagementsResponse> =>
-      config.fake ? Promise.resolve({ totalResults: 0, data: [] }) : volunteeringDB.filterEngagements(parameters),
+      config.fake
+        ? Promise.resolve({ totalResults: 0, data: [] })
+        : Sentry.startNewTrace(() => volunteeringDB.filteredEngagements(parameters)),
     engagementRecommendations: (
       // deno-lint-ignore no-explicit-any
       _parent: any,
       parameters: EngagementRecosParameters,
     ): Promise<EngagementsResponse> =>
-      config.fake ? Promise.resolve({ totalResults: 0, data: [] }) : volunteeringDB.findRecommendations(parameters),
+      config.fake
+        ? Promise.resolve({ totalResults: 0, data: [] })
+        : Sentry.startNewTrace(() => volunteeringDB.engagementRecommendations(parameters)),
     engagementRecos: (
       // deno-lint-ignore no-explicit-any
       _parent: any,
       parameters: EngagementRecosParameters,
     ): Promise<EngagementRecosResponse> =>
-      config.fake ? Promise.resolve({ totalResults: 0, data: [] }) : volunteeringDB.findRecos(parameters),
+      config.fake
+        ? Promise.resolve({ totalResults: 0, data: [] })
+        : Sentry.startNewTrace(() => volunteeringDB.engagementRecos(parameters)),
   },
   Mutation: {
     trackEngagementView: (
@@ -182,29 +189,6 @@ const createResolvers = (
   },
 });
 
-export const DEFAULT_PORT = 8004;
-export const DEFAULT_CACHE_ENABLED = true;
-export const DEFAULT_CACHE_TTL_MS_VOLTASTICS = 86_400_000; // 24 hours
-export const DEFAULT_CACHE_TTL_MS_DB = 3_600_000; // 1 hour
-
-export const DEFAULT_DB_CONNECTION_ATTEMPTS = 10;
-
-export interface VoltasticsConfig {
-  baseUrl: string;
-  apiToken: string;
-}
-export interface ServerConfig {
-  port: number; // default: 8004
-  cacheEnabled: boolean; // default: true
-  cacheTtlMsVoltastics: number; // default: 24 hours
-  cacheTtlMsDB: number; // default: 1 hour
-  voltastics: VoltasticsConfig;
-  imageProxyBaseUrl: string;
-  fake: boolean; // For local development. If set, the API returns dummy data
-  volunteeringDB: VolunteeringDBConfig;
-  geoAPIEndpointUrl: string;
-}
-
 export const createGraphQLServer = (config: ServerConfig, volunteeringDB: VolunteeringDB): GraphQLServer => {
   const plugins = config.cacheEnabled
     ? [
@@ -260,7 +244,7 @@ export const startServer = (config: ServerConfig): Deno.HttpServer<Deno.NetAddr>
       debug: {
         queries: false,
         notices: false,
-        results: true,
+        results: false,
         queryInError: true,
       },
     },
@@ -268,7 +252,7 @@ export const startServer = (config: ServerConfig): Deno.HttpServer<Deno.NetAddr>
       attempts: config.volunteeringDB.connectionAttempts,
     },
   });
-  const geoAPIClient = new GeoAPIClient(config.geoAPIEndpointUrl);
+  const geoAPIClient = new GeoAPIClientImpl(config.geoAPIEndpointUrl);
   const volunteeringDB = new PostgresVolunteeringDB(
     client,
     config.imageProxyBaseUrl,
@@ -280,7 +264,6 @@ export const startServer = (config: ServerConfig): Deno.HttpServer<Deno.NetAddr>
     hostname: "0.0.0.0",
     handler: (req: Request) => {
       const url = new URL(req.url);
-      console.debug(url.pathname);
       if (url.pathname.startsWith("/liveness")) return isDbConnectionAlive(client);
       else return graphQLServer.handleRequest(req);
     },
diff --git a/app/voltastics.ts b/app/voltastics.ts
index 8a6b7279cf4c3aba482d68400fcecf0b542329f1..e737eda2899cee1301df9db73f16ddcedfe16410 100644
--- a/app/voltastics.ts
+++ b/app/voltastics.ts
@@ -6,7 +6,6 @@ import {
   ApiSearchEngagementsResponse,
 } from "./api_types.ts";
 import { logger } from "./logging.ts";
-import { ServerConfig, VoltasticsConfig } from "./server.ts";
 import {
   CategoriesResponse,
   Category,
@@ -18,6 +17,7 @@ import {
   TrackEngagementViewResponse,
 } from "./types.ts";
 import { calculateRadius } from "./utils.ts";
+import { ServerConfig, VoltasticsConfig } from "./config.ts";
 
 const transformOrganizer = (
   engagement: ApiSearchEngagement,
diff --git a/app/voltastics_test.ts b/app/voltastics_test.ts
index 1ec4bc77d127a693e0486d3260bba50b5fc1f407..b8d53cf3780b3a674506d8e3363a4e11a33f2df3 100644
--- a/app/voltastics_test.ts
+++ b/app/voltastics_test.ts
@@ -20,7 +20,7 @@ import {
   EngagementsResponse,
   FilteredEngagementsParameters,
 } from "./types.ts";
-import { createGraphQLServer, GraphQLServer, ServerConfig, VoltasticsConfig } from "./server.ts";
+import { createGraphQLServer, GraphQLServer } from "./server.ts";
 
 import {
   apiCategoriesResponse,
@@ -32,6 +32,7 @@ import {
 import { fetchCategories, fetchEngagementOpportunities } from "./voltastics.ts";
 import { VolunteeringDB } from "./volunteering_db.ts";
 import { logger, LogSeverity } from "./logging.ts";
+import { ServerConfig, VoltasticsConfig } from "./config.ts";
 
 logger.setUpLogger(
   "development",
@@ -151,16 +152,16 @@ const noCacheServerConfig = {
 };
 
 const fakeVolunteeringDB = new (class implements VolunteeringDB {
-  public engagementById(_params: EngagementParameters): Promise<EngagementResponse> {
+  public engagement(_params: EngagementParameters): Promise<EngagementResponse> {
     return Promise.reject(new Error("should not have been called"));
   }
-  findRecommendations(_params: EngagementRecosParameters): Promise<EngagementsResponse> {
+  engagementRecommendations(_params: EngagementRecosParameters): Promise<EngagementsResponse> {
     return Promise.reject(new Error("should not have been called"));
   }
-  findRecos(_params: EngagementRecosParameters): Promise<EngagementRecosResponse> {
+  engagementRecos(_params: EngagementRecosParameters): Promise<EngagementRecosResponse> {
     return Promise.reject(new Error("should not have been called"));
   }
-  filterEngagements(_params: FilteredEngagementsParameters): Promise<EngagementsResponse> {
+  filteredEngagements(_params: FilteredEngagementsParameters): Promise<EngagementsResponse> {
     return Promise.reject(new Error("should not have been called"));
   }
 })();
diff --git a/app/volunteering_db.ts b/app/volunteering_db.ts
index b52a0a67f870aa2beea346aa867a25e6d3703d7a..4a1f60be5d39f1d04641ceb873c17a0e706da105 100644
--- a/app/volunteering_db.ts
+++ b/app/volunteering_db.ts
@@ -1,5 +1,5 @@
 import type { QueryObjectResult } from "./deps.ts";
-import { Client, GraphQLError } from "./deps.ts";
+import { Client, GraphQLError, Sentry } from "./deps.ts";
 import { GeoAPIClient } from "./geo_api_client.ts";
 import {
   Engagement,
@@ -312,10 +312,10 @@ const extractTotalResultCount = (
 const ERROR_CODE_NOT_FOUND = "NOT_FOUND";
 
 export interface VolunteeringDB {
-  engagementById(params: EngagementParameters): Promise<EngagementResponse>;
-  findRecommendations(params: EngagementRecosParameters): Promise<EngagementsResponse>;
-  findRecos(params: EngagementRecosParameters): Promise<EngagementRecosResponse>;
-  filterEngagements(params: FilteredEngagementsParameters): Promise<EngagementsResponse>;
+  engagement(params: EngagementParameters): Promise<EngagementResponse>;
+  engagementRecommendations(params: EngagementRecosParameters): Promise<EngagementsResponse>;
+  engagementRecos(params: EngagementRecosParameters): Promise<EngagementRecosResponse>;
+  filteredEngagements(params: FilteredEngagementsParameters): Promise<EngagementsResponse>;
 }
 
 export class PostgresVolunteeringDB implements VolunteeringDB {
@@ -333,65 +333,77 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     this.geoAPIClient = geoAPIClient;
   }
 
-  async engagementById({ id }: EngagementParameters): Promise<EngagementResponse> {
-    const before = Date.now();
-    const result = await this.client.queryObject<VolunteeringDBRow>`
-      SELECT * FROM volunteering_voltastics_with_classification
-      WHERE id = ${id}`;
-    const after = Date.now();
-    logger.debug(`[${(after - before).toString().padStart(4, " ")} ms] Successfully retrieved engagement by ID ${id}`);
-    if (result.rows.length < 1) {
-      return Promise.reject(
-        new GraphQLError("Not found", {
-          extensions: { "code": ERROR_CODE_NOT_FOUND },
-        }),
-      );
-    } else {
-      const row = result.rows[0];
-      return toEngagement(this.imageProxyBaseUrl, row);
-    }
+  async engagement({ id }: EngagementParameters): Promise<EngagementResponse> {
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.engagement" },
+      async () => {
+        const result = await this.client.queryObject<VolunteeringDBRow>`
+          SELECT * FROM volunteering_voltastics_with_classification
+          WHERE id = ${id}`;
+        if (result.rows.length < 1) {
+          return Promise.reject(
+            new GraphQLError("Not found", {
+              extensions: { "code": ERROR_CODE_NOT_FOUND },
+            }),
+          );
+        } else {
+          const row = result.rows[0];
+          return toEngagement(this.imageProxyBaseUrl, row);
+        }
+      },
+    );
   }
 
-  async findRecommendations(
+  async engagementRecommendations(
     { offset, limit, topics, skills, geolocationId }: EngagementRecosParameters,
   ): Promise<EngagementsResponse> {
-    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) =>
-      addQueryMatchInfo(row, toEngagement(this.imageProxyBaseUrl, row), topics, skills)
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.engagementRecommendations" },
+      async () => {
+        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) =>
+          addQueryMatchInfo(row, toEngagement(this.imageProxyBaseUrl, row), topics, skills)
+        );
+        return Promise.resolve({
+          totalResults: extractTotalResultCount(result),
+          data: recos,
+        });
+      },
     );
-    return Promise.resolve({
-      totalResults: extractTotalResultCount(result),
-      data: recos,
-    });
   }
 
-  async findRecos(
+  async engagementRecos(
     { 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,
-    });
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.engagementRecos" },
+      async () => {
+        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(
@@ -401,18 +413,8 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     skills: string[],
     geolocationId?: string,
   ): Promise<QueryObjectResult<VolunteeringDBRow>> {
-    const beforeResolve = Date.now();
     const geolocationCoordinates = geolocationId
       ? await this.geoAPIClient.resolveCoordinates(geolocationId)
-        .then((coordinates) => {
-          const afterResolve = Date.now();
-          logger.debug(
-            `[${(afterResolve - beforeResolve).toString().padStart(4, " ")} ms] Successfully resolved ${
-              JSON.stringify(coordinates)
-            } for geolocationId=${geolocationId}`,
-          );
-          return coordinates;
-        })
         .catch((error) => {
           logger.warn(error);
           if (topics.length == 0 && skills.length == 0) {
@@ -425,19 +427,6 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
         })
       : undefined;
 
-    const beforeQuery = Date.now();
-    const logQueryDuration = (
-      result: QueryObjectResult<VolunteeringDBRow>,
-    ): QueryObjectResult<VolunteeringDBRow> => {
-      const afterQuery = Date.now();
-      logger.debug(
-        `[${
-          (afterQuery - beforeQuery).toString().padStart(4, " ")
-        } ms] Successfully retrieved ${result.rows.length} recommendations from DB`,
-      );
-      return result;
-    };
-
     if (geolocationCoordinates && (topics.length > 0 || skills.length > 0)) {
       return this.queryRecosBasedOnTopicsSkillsAndLocation(
         offset,
@@ -445,14 +434,12 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
         topics,
         skills,
         geolocationCoordinates,
-      ).then(logQueryDuration);
+      );
     } else if (geolocationCoordinates) {
       return this.queryRecosBasedOnLocation(
         offset,
         limit,
         geolocationCoordinates,
-      ).then(
-        logQueryDuration,
       );
     } else if (topics.length > 0 || skills.length > 0) {
       return this.queryRecosBasedOnTopicsAndSkills(
@@ -460,7 +447,7 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
         limit,
         topics,
         skills,
-      ).then(logQueryDuration);
+      );
     } else {
       return Promise.reject(
         "It is required to provide at least one of (topics, skills, or location) in order to retrieve recommendations",
@@ -474,21 +461,23 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     topics: string[],
     skills: string[],
   ): Promise<QueryObjectResult<VolunteeringDBRow>> {
-    logger.debug(
-      `queryRecosBasedOnTopicsAndSkills ${JSON.stringify({ offset, limit, topics, skills })}`,
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.queryRecosBasedOnTopicsAndSkills", op: "db.query" },
+      async () => {
+        logger.debug(
+          `queryRecosBasedOnTopicsAndSkills ${JSON.stringify({ offset, limit, topics, skills })}`,
+        );
+        const queryVector = JSON.stringify(queryEmbeddingVector(topics, skills));
+        return await this.client.queryObject<VolunteeringDBRow>`
+          WITH vector_matches AS (SELECT *, 1 - (embedding_array <=> ${queryVector}) AS cosine_similarity
+                                  FROM volunteering_voltastics_with_classification
+                                  ORDER BY cosine_similarity DESC)
+          SELECT *, count(*) OVER () AS total_results
+          FROM vector_matches
+          OFFSET ${offset} LIMIT ${limit};
+        `;
+      },
     );
-    const queryVector = JSON.stringify(queryEmbeddingVector(topics, skills));
-    return await this.client.queryObject<VolunteeringDBRow>`
-      WITH vector_matches AS (
-        SELECT *, 1 - (embedding_array <=> ${queryVector}) AS cosine_similarity
-        FROM volunteering_voltastics_with_classification
-        ORDER BY cosine_similarity DESC
-      )
-      SELECT *, count(*) OVER() AS total_results
-      FROM vector_matches
-      OFFSET ${offset}
-      LIMIT ${limit};
-    `;
   }
 
   private async queryRecosBasedOnTopicsSkillsAndLocation(
@@ -498,24 +487,27 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     skills: string[],
     geolocationCoordinates: GeolocationCoordinates,
   ): Promise<QueryObjectResult<VolunteeringDBRow>> {
-    logger.debug(
-      `queryRecosBasedOnTopicsSkillsAndLocation ${
-        JSON.stringify({ offset, limit, topics, skills, geolocationCoordinates })
-      }`,
-    );
-    const rankingWeightCosineSimilarity = 0.7; // Weight for cosine similarity
-    const rankingWeightDistance = 0.3; // Weight for proximity
-    const maxDistanceInMeters = 50_000; // in meters (e.g., 50 km)
-    const queryVector = JSON.stringify(queryEmbeddingVector(topics, skills));
-    const { lat, lon } = geolocationCoordinates;
-
-    // Useful knowledge
-    // - PostGIS uses lon, lat (NOT lat, lon)
-    // - <#> uses index scans if used in an ORDER BY clause, ST_Distance does not
-    // - Database stores in GPS-Coordinates (SRID 4326)
-    // - In order to calculate a distance in meters, a geometry needs to be projected by transforming using
-    //   ST_Transform(geom, 3857) where we use the SRID 3857 for Pseudo-Mercator
-    return await this.client.queryObject<VolunteeringDBRow>`
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.queryRecosBasedOnTopicsSkillsAndLocation", op: "db.query" },
+      async () => {
+        logger.debug(
+          `queryRecosBasedOnTopicsSkillsAndLocation ${
+            JSON.stringify({ offset, limit, topics, skills, geolocationCoordinates })
+          }`,
+        );
+        const rankingWeightCosineSimilarity = 0.7; // Weight for cosine similarity
+        const rankingWeightDistance = 0.3; // Weight for proximity
+        const maxDistanceInMeters = 50_000; // in meters (e.g., 50 km)
+        const queryVector = JSON.stringify(queryEmbeddingVector(topics, skills));
+        const { lat, lon } = geolocationCoordinates;
+
+        // Useful knowledge
+        // - PostGIS uses lon, lat (NOT lat, lon)
+        // - <#> uses index scans if used in an ORDER BY clause, ST_Distance does not
+        // - Database stores in GPS-Coordinates (SRID 4326)
+        // - In order to calculate a distance in meters, a geometry needs to be projected by transforming using
+        //   ST_Transform(geom, 3857) where we use the SRID 3857 for Pseudo-Mercator
+        return await this.client.queryObject<VolunteeringDBRow>`
       WITH calculations AS (
         SELECT *,
               1 - (embedding_array <=> ${queryVector}) AS cosine_similarity,
@@ -534,6 +526,8 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
       OFFSET ${offset}
       LIMIT ${limit};
     `;
+      },
+    );
   }
 
   private async queryRecosBasedOnLocation(
@@ -541,11 +535,14 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     limit: number,
     geolocationCoordinates: GeolocationCoordinates,
   ): Promise<QueryObjectResult<VolunteeringDBRow>> {
-    logger.debug(
-      `queryRecosBasedOnLocation ${JSON.stringify({ offset, limit, geolocationCoordinates })}`,
-    );
-    const { lat, lon } = geolocationCoordinates;
-    return await this.client.queryObject<VolunteeringDBRow>`
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.queryRecosBasedOnLocation", op: "db.query" },
+      async () => {
+        logger.debug(
+          `queryRecosBasedOnLocation ${JSON.stringify({ offset, limit, geolocationCoordinates })}`,
+        );
+        const { lat, lon } = geolocationCoordinates;
+        return await this.client.queryObject<VolunteeringDBRow>`
       SELECT *,
              count(*) OVER() AS total_results,
              ST_Transform(location_gps, 3857) <#> ST_Transform(ST_SetSRID(ST_MakePoint(${lon.toString()}, ${lat.toString()}), 4326), 3857) AS distance_in_meters
@@ -554,9 +551,11 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
       OFFSET ${offset}
       LIMIT ${limit};
     `;
+      },
+    );
   }
 
-  async filterEngagements(
+  async filteredEngagements(
     { offset, limit, topics, skills, geolocationId }: FilteredEngagementsParameters,
   ): Promise<EngagementsResponse> {
     const geometry: GeolocationGeometry | undefined = geolocationId
@@ -592,56 +591,52 @@ export class PostgresVolunteeringDB implements VolunteeringDB {
     limit: number,
     geometry?: GeolocationGeometry,
   ): Promise<EngagementsResponse> {
-    const queryVector = queryEmbeddingVector(topics, skills);
-    const queryVectorString = JSON.stringify(queryVector);
-    const nonEmptyQueryVector = topics.length + skills.length > 0;
-    const binaryQueryVector = queryVector.map((value) => value > 0 ? "1" : "0")
-      .join("");
-    const emptyBinaryQueryVector = "0".repeat(EMBEDDING_DIMENSIONS);
-    const beforeQuery = Date.now();
-
-    // Filtering engagements:
-    // - Precomputed `binary_embedding` is a bit string marking matching topics and skills
-    // - Unset bits in `binary_embedding` for terms that are not part of the query using bitwise AND => If at least one set bit remains, it matches the query (checked by comparing to empty bit string, as it's too long for integer conversion)
-    // - Filter by geolocation by checking if coordinates are inside the given geolocation's GeoJSON geometry
-    // - Sort results using cosine_similarity
-    const result = await this.client.queryObject<VolunteeringDBRow>`
-      WITH vector_matches AS (
-        SELECT
-          *,
-          1 - (embedding_array <=> ${queryVectorString}) AS cosine_similarity,
-          CASE
-            WHEN ${nonEmptyQueryVector}
-            THEN (binary_embedding & ${binaryQueryVector}) > ${emptyBinaryQueryVector}
-            ELSE true
-          END AS matches_query,
-          CASE
-            WHEN ${!!geometry}
-            THEN ST_Contains(ST_GeomFromGeoJSON(${geometry}), location_gps)
-            ELSE true
-          END AS matches_location
-        FROM volunteering_voltastics_with_classification
-        ORDER BY cosine_similarity DESC
-      )
-      SELECT *, count(*) OVER() AS total_results
-      FROM vector_matches
-      WHERE matches_query
-      AND matches_location
-      OFFSET ${offset}
-      LIMIT ${limit};
-    `;
-
-    const afterQuery = Date.now();
-    const totalResults = extractTotalResultCount(result);
-    logger.debug(
-      `[${
-        (afterQuery - beforeQuery).toString().padStart(4, " ")
-      } ms] Successfully fetched ${result.rows.length}/${totalResults} filtered engagements from DB`,
+    return await Sentry.startSpan(
+      { name: "VolunteeringDB.queryFilteredEngagements", op: "db.query" },
+      async () => {
+        const queryVector = queryEmbeddingVector(topics, skills);
+        const queryVectorString = JSON.stringify(queryVector);
+        const nonEmptyQueryVector = topics.length + skills.length > 0;
+        const binaryQueryVector = queryVector.map((value) => value > 0 ? "1" : "0")
+          .join("");
+        const emptyBinaryQueryVector = "0".repeat(EMBEDDING_DIMENSIONS);
+
+        // Filtering engagements:
+        // - Precomputed `binary_embedding` is a bit string marking matching topics and skills
+        // - Unset bits in `binary_embedding` for terms that are not part of the query using bitwise AND => If at least one set bit remains, it matches the query (checked by comparing to empty bit string, as it's too long for integer conversion)
+        // - Filter by geolocation by checking if coordinates are inside the given geolocation's GeoJSON geometry
+        // - Sort results using cosine_similarity
+        const result = await this.client.queryObject<VolunteeringDBRow>`
+          WITH vector_matches AS (
+            SELECT
+              *,
+              1 - (embedding_array <=> ${queryVectorString}) AS cosine_similarity,
+              CASE
+                WHEN ${nonEmptyQueryVector}
+                THEN (binary_embedding & ${binaryQueryVector}) > ${emptyBinaryQueryVector}
+                ELSE true
+              END AS matches_query,
+              CASE
+                WHEN ${!!geometry}
+                THEN ST_Contains(ST_GeomFromGeoJSON(${geometry}), location_gps)
+                ELSE true
+              END AS matches_location
+            FROM volunteering_voltastics_with_classification
+            ORDER BY cosine_similarity DESC
+          )
+          SELECT *, count(*) OVER() AS total_results
+          FROM vector_matches
+          WHERE matches_query
+          AND matches_location
+          OFFSET ${offset}
+          LIMIT ${limit};
+        `;
+
+        return {
+          totalResults: extractTotalResultCount(result),
+          data: result.rows.map((row) => toEngagement(this.imageProxyBaseUrl, row)),
+        };
+      },
     );
-
-    return {
-      totalResults,
-      data: result.rows.map((row) => toEngagement(this.imageProxyBaseUrl, row)),
-    };
   }
 }
diff --git a/app/volunteering_db_test.ts b/app/volunteering_db_test.ts
index a7bdc079dac127ea243f6e3fd5df67dbd67aa320..c1edef5114ac805e02737a85e44687367e25661b 100644
--- a/app/volunteering_db_test.ts
+++ b/app/volunteering_db_test.ts
@@ -305,14 +305,14 @@ describe("VolunteeringDB", () => {
   describe("querying engagement by id", () => {
     it("correctly parses engagement", () => {
       withMockedDependencies([dbFixtures.row1])(async (volunteeringDB) => {
-        const response = await volunteeringDB.engagementById({ id: "1" });
+        const response = await volunteeringDB.engagement({ id: "1" });
         assertEngagementMatchesRow(dbFixtures.row1, response!);
       });
     });
 
     it("throws NOT_FOUND error for empty result", () => {
       withMockedDependencies([])(async (volunteeringDB) => {
-        const query = () => volunteeringDB.engagementById({ id: "1" });
+        const query = () => volunteeringDB.engagement({ id: "1" });
         await assertRejects(query, GraphQLError, "Not found");
       });
     });
@@ -322,7 +322,7 @@ describe("VolunteeringDB", () => {
     it("resolves latitude/longitude using the GeoAPI client when geolocationId is given", () => {
       withMockedDependencies()(
         (volunteeringDB, geoAPIClient) => {
-          volunteeringDB.findRecommendations({
+          volunteeringDB.engagementRecommendations({
             limit: 10,
             offset: 0,
             topics: [],
@@ -338,7 +338,7 @@ describe("VolunteeringDB", () => {
     it("does not resolve latitude/longitude when geolocationId is not given", () => {
       withMockedDependencies()(
         (volunteeringDB, geoAPIClient) => {
-          volunteeringDB.findRecommendations({
+          volunteeringDB.engagementRecommendations({
             limit: 10,
             offset: 0,
             topics: ["agriculture-food"],
@@ -357,7 +357,7 @@ describe("VolunteeringDB", () => {
             returnsNext([Promise.reject("boom!")]),
           );
 
-          const result = await volunteeringDB.findRecommendations({
+          const result = await volunteeringDB.engagementRecommendations({
             limit: 10,
             offset: 0,
             topics: ["agriculture-food"],
@@ -379,7 +379,7 @@ describe("VolunteeringDB", () => {
           );
           await assertRejects(
             () =>
-              volunteeringDB.findRecommendations({
+              volunteeringDB.engagementRecommendations({
                 limit: 10,
                 offset: 0,
                 topics: [],
@@ -396,7 +396,7 @@ describe("VolunteeringDB", () => {
   describe("findRecos", () => {
     it("wraps search results with matchedTopics, matchedSkills and matchedGeoLocationDistanceInMeters", () => {
       withMockedDependencies([dbFixtures.row1])(async (volunteeringDB, _geoApiClient) => {
-        const response = await volunteeringDB.findRecos({
+        const response = await volunteeringDB.engagementRecos({
           limit: 10,
           offset: 0,
           topics: ["disabilities-inclusion", "water-ocean"],
@@ -425,7 +425,7 @@ describe("VolunteeringDB", () => {
     it("resolves latitude/longitude using the GeoAPI client when geolocationId is given", () => {
       withMockedDependencies()(
         (volunteeringDB, geoAPIClient) => {
-          volunteeringDB.findRecos({
+          volunteeringDB.engagementRecos({
             limit: 10,
             offset: 0,
             topics: [],
@@ -441,7 +441,7 @@ describe("VolunteeringDB", () => {
     it("does not resolve latitude/longitude when geolocationId is not given", () => {
       withMockedDependencies()(
         (volunteeringDB, geoAPIClient) => {
-          volunteeringDB.findRecos({
+          volunteeringDB.engagementRecos({
             limit: 10,
             offset: 0,
             topics: ["agriculture-food"],
@@ -460,7 +460,7 @@ describe("VolunteeringDB", () => {
             returnsNext([Promise.reject("boom!")]),
           );
 
-          const result = await volunteeringDB.findRecos({
+          const result = await volunteeringDB.engagementRecos({
             limit: 10,
             offset: 0,
             topics: ["agriculture-food"],
@@ -482,7 +482,7 @@ describe("VolunteeringDB", () => {
           );
           await assertRejects(
             () =>
-              volunteeringDB.findRecos({
+              volunteeringDB.engagementRecos({
                 limit: 10,
                 offset: 0,
                 topics: [],
@@ -501,7 +501,7 @@ describe("VolunteeringDB", () => {
     it("resolves geolocation geometry using the GeoAPI client when geolocationId is given", () => {
       withMockedDependencies()(
         (volunteeringDB, geoAPIClient) => {
-          volunteeringDB.filterEngagements({
+          volunteeringDB.filteredEngagements({
             limit: 10,
             offset: 0,
             topics: [],
@@ -517,7 +517,7 @@ describe("VolunteeringDB", () => {
     it("does not resolve geolocation geometry when geolocationId is not given", () => {
       withMockedDependencies()(
         (volunteeringDB, geoAPIClient) => {
-          volunteeringDB.filterEngagements({
+          volunteeringDB.filteredEngagements({
             limit: 10,
             offset: 0,
             topics: ["agriculture-food"],
@@ -537,7 +537,7 @@ describe("VolunteeringDB", () => {
           );
           await assertRejects(
             () =>
-              volunteeringDB.filterEngagements({
+              volunteeringDB.filteredEngagements({
                 limit: 10,
                 offset: 0,
                 topics: ["agriculture-food"],
@@ -555,7 +555,7 @@ describe("VolunteeringDB", () => {
         async (volunteeringDB, _geoAPIClient) => {
           await assertRejects(
             () =>
-              volunteeringDB.filterEngagements({
+              volunteeringDB.filteredEngagements({
                 limit: 10,
                 offset: 0,
                 topics: [],
diff --git a/deno.lock b/deno.lock
index 0edcbcb380b38efcb3f1e4ca75903a76b8e8be39..1ad9d407d1463645f7d7408f7a7a3c4f3cde324a 100644
--- a/deno.lock
+++ b/deno.lock
@@ -118,7 +118,7 @@
         "dependencies": {
           "@graphql-typed-document-node/core": "@graphql-typed-document-node/core@3.2.0_graphql@16.8.1",
           "cross-inspect": "cross-inspect@1.0.1",
-          "dset": "dset@3.1.3",
+          "dset": "dset@3.1.4",
           "graphql": "graphql@16.8.1",
           "tslib": "tslib@2.7.0"
         }
@@ -320,8 +320,8 @@
           "tslib": "tslib@2.7.0"
         }
       },
-      "dset@3.1.3": {
-        "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==",
+      "dset@3.1.4": {
+        "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==",
         "dependencies": {}
       },
       "fast-decode-uri-component@1.0.1": {
@@ -356,7 +356,7 @@
           "@graphql-yoga/subscription": "@graphql-yoga/subscription@3.1.0",
           "@whatwg-node/fetch": "@whatwg-node/fetch@0.8.8",
           "@whatwg-node/server": "@whatwg-node/server@0.7.7",
-          "dset": "dset@3.1.3",
+          "dset": "dset@3.1.4",
           "graphql": "graphql@16.8.1",
           "lru-cache": "lru-cache@7.18.3",
           "tslib": "tslib@2.7.0"
@@ -373,7 +373,7 @@
           "@graphql-yoga/subscription": "@graphql-yoga/subscription@5.0.1",
           "@whatwg-node/fetch": "@whatwg-node/fetch@0.9.21",
           "@whatwg-node/server": "@whatwg-node/server@0.9.49",
-          "dset": "dset@3.1.3",
+          "dset": "dset@3.1.4",
           "graphql": "graphql@16.8.1",
           "lru-cache": "lru-cache@10.4.3",
           "tslib": "tslib@2.7.0"
@@ -457,7 +457,8 @@
   },
   "redirects": {
     "https://deno.land/std/async/mod.ts": "https://deno.land/std@0.224.0/async/mod.ts",
-    "https://deno.land/x/postgres/mod.ts": "https://deno.land/x/postgres@v0.19.3/mod.ts"
+    "https://deno.land/x/postgres/mod.ts": "https://deno.land/x/postgres@v0.19.3/mod.ts",
+    "https://deno.land/x/sentry/index.mjs": "https://deno.land/x/sentry@8.30.0/index.mjs"
   },
   "remote": {
     "https://deno.land/std@0.155.0/fmt/colors.ts": "ff7dc9c9f33a72bd48bc24b21bbc1b4545d8494a431f17894dbc5fe92a938fc4",
@@ -467,17 +468,6 @@
     "https://deno.land/std@0.155.0/testing/asserts.ts": "ac295f7fd22a7af107580e2475402a8c386cb1bf18bf837ae266ac0665786026",
     "https://deno.land/std@0.155.0/testing/bdd.ts": "35060cefd9cc21b414f4d89453b3551a3d52ec50aeff25db432503c5485b2f72",
     "https://deno.land/std@0.155.0/testing/mock.ts": "d7ad40139cda87476c4dc1efeffe406bbfb4a5f82b688e0b85bffe9e911526a2",
-    "https://deno.land/std@0.156.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06",
-    "https://deno.land/std@0.156.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0",
-    "https://deno.land/std@0.156.0/async/debounce.ts": "de5433bff08a2bb61416fc53b3bd2d5867090c8a815465e5b4a10a77495b1051",
-    "https://deno.land/std@0.156.0/async/deferred.ts": "c01de44b9192359cebd3fe93273fcebf9e95110bf3360023917da9a2d1489fae",
-    "https://deno.land/std@0.156.0/async/delay.ts": "0419dfc993752849692d1f9647edf13407c7facc3509b099381be99ffbc9d699",
-    "https://deno.land/std@0.156.0/async/mod.ts": "dd0a8ed4f3984ffabe2fcca7c9f466b7932d57b1864ffee148a5d5388316db6b",
-    "https://deno.land/std@0.156.0/async/mux_async_iterator.ts": "3447b28a2a582224a3d4d3596bccbba6e85040da3b97ed64012f7decce98d093",
-    "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",
@@ -590,8 +580,6 @@
     "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",
@@ -615,6 +603,7 @@
     "https://deno.land/x/postgres@v0.19.3/query/types.ts": "540f6f973d493d63f2c0059a09f3368071f57931bba68bea408a635a3e0565d6",
     "https://deno.land/x/postgres@v0.19.3/utils/deferred.ts": "5420531adb6c3ea29ca8aac57b9b59bd3e4b9a938a4996bbd0947a858f611080",
     "https://deno.land/x/postgres@v0.19.3/utils/utils.ts": "ca47193ea03ff5b585e487a06f106d367e509263a960b787197ce0c03113a738",
+    "https://deno.land/x/sentry@8.30.0/index.mjs": "b043c7d7ad8a67d7c554f46743e9c8d44c673aabe1e28e51c96187b02c30e4ab",
     "https://esm.sh/@turf/turf@6.5.0": "1b89eb65070928dd870eb2317386f35192ccc1145242946ba7725432eb5fe547",
     "https://esm.sh/v135/@turf/along@6.5.0/denonext/along.mjs": "c0b37a23533598d200708d3f036820ac40979b430b5f0bc28611a7348e2a7c94",
     "https://esm.sh/v135/@turf/angle@6.5.0/denonext/angle.mjs": "347a7b8ad0bd322092d0625bc59e19df1fa066f78f51182c22d070eb705a0e20",
diff --git a/terraform/environments/deployment.tf b/terraform/environments/deployment.tf
index 00cc1e712dd31727eb0532e48a639df3d98e5298..f8a86aa22b46a42aec5d053f7a33c6d51f40123e 100644
--- a/terraform/environments/deployment.tf
+++ b/terraform/environments/deployment.tf
@@ -65,6 +65,10 @@ resource "google_cloud_run_service" "volunteering_api" {
           name  = "ENVIRONMENT"
           value = local.environment
         }
+        env {
+          name  = "SENTRY_DSN"
+          value = "https://030790361bc255b6e93a562c1a6ddba8@o949544.ingest.us.sentry.io/4507966078713856"
+        }
         env {
           name  = "CACHE_ENABLED"
           value = "true"