diff --git a/pom.xml b/pom.xml
index 84b39cead..61c851e3c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -332,6 +332,12 @@
pom
import
+
+ earth.adi
+ testcontainers-foundationdb
+ 1.1.0
+ test
+
diff --git a/service/pom.xml b/service/pom.xml
index 41a7e6718..40ce2b6ee 100644
--- a/service/pom.xml
+++ b/service/pom.xml
@@ -502,6 +502,12 @@
test
+
+ earth.adi
+ testcontainers-foundationdb
+ test
+
+
com.google.auth
google-auth-library-oauth2-http
@@ -733,6 +739,7 @@
-javaagent:${org.mockito:mockito-core:jar} --add-opens=java.base/java.net=ALL-UNNAMED
+ ${foundationdb.version}
${localstack.image}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java
new file mode 100644
index 000000000..96bfa113c
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbDatabaseLifecycleManager.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.storage;
+
+import com.apple.foundationdb.Database;
+import com.apple.foundationdb.FDB;
+import java.io.IOException;
+
+interface FoundationDbDatabaseLifecycleManager {
+
+ void initializeDatabase(final FDB fdb) throws IOException;
+
+ Database getDatabase();
+
+ void closeDatabase();
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java
new file mode 100644
index 000000000..9c87e471d
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbExtension.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.storage;
+
+import com.apple.foundationdb.Database;
+import com.apple.foundationdb.FDB;
+import java.io.IOException;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+class FoundationDbExtension implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {
+
+ private static FoundationDbDatabaseLifecycleManager databaseLifecycleManager;
+
+ @Override
+ public void beforeAll(final ExtensionContext context) throws IOException {
+ if (databaseLifecycleManager == null) {
+ final String serviceContainerName = System.getProperty("foundationDb.serviceContainerName");
+
+ databaseLifecycleManager = serviceContainerName != null
+ ? new ServiceContainerFoundationDbDatabaseLifecycleManager(serviceContainerName)
+ : new TestcontainersFoundationDbDatabaseLifecycleManager();
+
+ databaseLifecycleManager.initializeDatabase(FDB.selectAPIVersion(730));
+
+ context.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put(getClass().getName(), this);
+ }
+ }
+
+ public Database getDatabase() {
+ return databaseLifecycleManager.getDatabase();
+ }
+
+ @Override
+ public void close() throws Throwable {
+ if (databaseLifecycleManager != null) {
+ databaseLifecycleManager.closeDatabase();
+ }
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java
new file mode 100644
index 000000000..f19d509bb
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/FoundationDbTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.storage;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.whispersystems.textsecuregcm.util.TestRandomUtil;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+public class FoundationDbTest {
+
+ @RegisterExtension
+ static FoundationDbExtension FOUNDATION_DB_EXTENSION = new FoundationDbExtension();
+
+ @Test
+ void setGetValue() {
+ final byte[] key = "test".getBytes(StandardCharsets.UTF_8);
+ final byte[] value = TestRandomUtil.nextBytes(16);
+
+ FOUNDATION_DB_EXTENSION.getDatabase().run(transaction -> {
+ transaction.set(key, value);
+ return null;
+ });
+
+ final byte[] retrievedValue = FOUNDATION_DB_EXTENSION.getDatabase().run(transaction -> transaction.get(key).join());
+
+ assertArrayEquals(value, retrievedValue);
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java
new file mode 100644
index 000000000..973771d17
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ServiceContainerFoundationDbDatabaseLifecycleManager.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.storage;
+
+import com.apple.foundationdb.Database;
+import com.apple.foundationdb.FDB;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages the lifecycle of a database connected to a FoundationDB instance running as an external service container.
+ */
+class ServiceContainerFoundationDbDatabaseLifecycleManager implements FoundationDbDatabaseLifecycleManager {
+
+ private final String foundationDbServiceContainerName;
+
+ private Database database;
+
+ private static final Logger log = LoggerFactory.getLogger(ServiceContainerFoundationDbDatabaseLifecycleManager.class);
+
+ ServiceContainerFoundationDbDatabaseLifecycleManager(final String foundationDbServiceContainerName) {
+ log.info("Using FoundationDB service container: {}", foundationDbServiceContainerName);
+ this.foundationDbServiceContainerName = foundationDbServiceContainerName;
+ }
+
+ @Override
+ public void initializeDatabase(final FDB fdb) throws IOException {
+ final File clusterFile = File.createTempFile("fdb.cluster", "");
+ clusterFile.deleteOnExit();
+
+ try (final FileWriter fileWriter = new FileWriter(clusterFile)) {
+ fileWriter.write(String.format("docker:docker@%s:4500", foundationDbServiceContainerName));
+ }
+
+ // If we don't initialize the database before trying to use it, things will just hang in a mysterious, message-free
+ // way. Note that the `new` keyword in `configure new single memory` means that we can't accidentally clobber an
+ // existing database (though initialization may fail if there's already a database present).
+ new ProcessBuilder("/usr/bin/fdbcli",
+ "-C", clusterFile.getAbsolutePath(),
+ "--exec", "configure new single memory")
+ .start()
+ .onExit()
+ .join();
+
+ database = fdb.open(clusterFile.getAbsolutePath());
+ }
+
+ @Override
+ public Database getDatabase() {
+ return database;
+ }
+
+ @Override
+ public void closeDatabase() {
+ database.close();
+ }
+}
diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java
new file mode 100644
index 000000000..cf458d8ad
--- /dev/null
+++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/TestcontainersFoundationDbDatabaseLifecycleManager.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.whispersystems.textsecuregcm.storage;
+
+import com.apple.foundationdb.Database;
+import com.apple.foundationdb.FDB;
+import earth.adi.testcontainers.containers.FoundationDBContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.utility.DockerImageName;
+
+class TestcontainersFoundationDbDatabaseLifecycleManager implements FoundationDbDatabaseLifecycleManager {
+
+ private FoundationDBContainer foundationDBContainer;
+ private Database database;
+
+ private static final String FOUNDATIONDB_IMAGE_NAME = "foundationdb/foundationdb:" +
+ System.getProperty("foundationdb.version", "7.3.62");
+
+ private static final Logger log = LoggerFactory.getLogger(TestcontainersFoundationDbDatabaseLifecycleManager.class);
+
+ @Override
+ public void initializeDatabase(final FDB fdb) {
+ log.info("Using Testcontainers FoundationDB container: {}", FOUNDATIONDB_IMAGE_NAME);
+
+ foundationDBContainer = new FoundationDBContainer(DockerImageName.parse(FOUNDATIONDB_IMAGE_NAME));
+ foundationDBContainer.start();
+
+ database = fdb.open(foundationDBContainer.getClusterFilePath());
+ }
+
+ @Override
+ public Database getDatabase() {
+ return database;
+ }
+
+ @Override
+ public void closeDatabase() {
+ database.close();
+ foundationDBContainer.close();
+ }
+}