diff --git a/core/src/main/java/google/registry/bsa/persistence/BsaTld.java b/core/src/main/java/google/registry/bsa/persistence/BsaTld.java
new file mode 100644
index 00000000000..d3effee649f
--- /dev/null
+++ b/core/src/main/java/google/registry/bsa/persistence/BsaTld.java
@@ -0,0 +1,55 @@
+// Copyright 2026 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.bsa.persistence;
+
+import google.registry.model.tld.Tld;
+import google.registry.persistence.VKey;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import java.time.Instant;
+
+/**
+ * A {@link Tld} that is ready to be included in BSA processing.
+ *
+ *
A {@code Tld}'s enrollment in BSA is controlled by configuration (See {@link
+ * Tld#getBsaEnrollStartTime}). This table records the {@code Tld}s that have been initialized.
+ */
+@Entity
+final class BsaTld {
+
+ @Id String tld;
+
+ /** The time when this tld was properly set up for BSA processing. */
+ @SuppressWarnings("unused") // Field is for record and troubleshooting.
+ @Column(nullable = false)
+ Instant readyTime;
+
+ @SuppressWarnings("unused") // For Hibernate
+ private BsaTld() {}
+
+ BsaTld(String tld, Instant readyTime) {
+ this.tld = tld;
+ this.readyTime = readyTime;
+ }
+
+ String getTld() {
+ return tld;
+ }
+
+ static VKey vKey(String tld) {
+ return VKey.create(BsaTld.class, tld);
+ }
+}
diff --git a/core/src/main/java/google/registry/bsa/persistence/Queries.java b/core/src/main/java/google/registry/bsa/persistence/Queries.java
index e22c13f7115..e6f73353d35 100644
--- a/core/src/main/java/google/registry/bsa/persistence/Queries.java
+++ b/core/src/main/java/google/registry/bsa/persistence/Queries.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.bsa.BsaStringUtils.DOMAIN_SPLITTER;
import static google.registry.bsa.BsaTransactions.bsaQuery;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
@@ -44,6 +45,11 @@ private static Object detach(Object obj) {
return obj;
}
+ /** Returns the names of all TLDs enrolled in BSA. */
+ public static ImmutableSet getEnrolledTlds() {
+ return tm().loadAllOf(BsaTld.class).stream().map(BsaTld::getTld).collect(toImmutableSet());
+ }
+
public static ImmutableList batchReadBsaLabelText(
Optional lastRead, int batchSize) {
return ImmutableList.copyOf(
diff --git a/core/src/main/resources/META-INF/persistence.xml b/core/src/main/resources/META-INF/persistence.xml
index 322ce06f8ff..d793ce648f7 100644
--- a/core/src/main/resources/META-INF/persistence.xml
+++ b/core/src/main/resources/META-INF/persistence.xml
@@ -39,6 +39,7 @@
google.registry.bsa.persistence.BsaDomainRefresh
google.registry.bsa.persistence.BsaDownload
google.registry.bsa.persistence.BsaLabel
+ google.registry.bsa.persistence.BsaTld
google.registry.bsa.persistence.BsaUnblockableDomain
google.registry.model.billing.BillingCancellation
google.registry.model.billing.BillingEvent
diff --git a/core/src/test/java/google/registry/bsa/persistence/BsaTldTest.java b/core/src/test/java/google/registry/bsa/persistence/BsaTldTest.java
new file mode 100644
index 00000000000..d11a077d7ed
--- /dev/null
+++ b/core/src/test/java/google/registry/bsa/persistence/BsaTldTest.java
@@ -0,0 +1,43 @@
+// Copyright 2023 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.bsa.persistence;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
+
+import google.registry.persistence.transaction.JpaTestExtensions;
+import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationWithCoverageExtension;
+import google.registry.testing.FakeClock;
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+/** Unit tests for {@link BsaLabel}. */
+public class BsaTldTest {
+
+ FakeClock fakeClock = new FakeClock(Instant.parse("2024-01-01T00:00:00Z"));
+
+ @RegisterExtension
+ final JpaIntegrationWithCoverageExtension jpa =
+ new JpaTestExtensions.Builder().withClock(fakeClock).buildIntegrationWithCoverageExtension();
+
+ @Test
+ void persist() {
+ tm().transact(() -> tm().put(new BsaTld("tld", fakeClock.now())));
+ BsaTld persisted = tm().transact(() -> tm().loadByKey(BsaTld.vKey("tld")));
+ assertThat(persisted.getTld()).isEqualTo("tld");
+ assertThat(persisted.readyTime).isEqualTo(fakeClock.now());
+ }
+}
diff --git a/core/src/test/java/google/registry/bsa/persistence/QueriesTest.java b/core/src/test/java/google/registry/bsa/persistence/QueriesTest.java
index f185aeb7cde..7c9507b9a20 100644
--- a/core/src/test/java/google/registry/bsa/persistence/QueriesTest.java
+++ b/core/src/test/java/google/registry/bsa/persistence/QueriesTest.java
@@ -77,6 +77,21 @@ void setup() {
});
}
+ @Test
+ void getEnrolledTlds_noneEnrolled() {
+ assertThat(tm().transact(Queries::getEnrolledTlds)).isEmpty();
+ }
+
+ @Test
+ void getEnrolledTlds_hasEnrolled() {
+ tm().transact(
+ () -> {
+ tm().put(new BsaTld("tld1", fakeClock.now()));
+ tm().put(new BsaTld("tld2", fakeClock.now()));
+ });
+ assertThat(tm().transact(Queries::getEnrolledTlds)).containsExactly("tld1", "tld2");
+ }
+
@Test
void queryBsaUnblockableDomainByLabels_oneLabel() {
assertThat(
diff --git a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
index 476f5d8e0e1..8616589387c 100644
--- a/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
+++ b/core/src/test/java/google/registry/schema/integration/SqlIntegrationTestSuite.java
@@ -19,6 +19,7 @@
import google.registry.bsa.persistence.BsaDomainRefreshTest;
import google.registry.bsa.persistence.BsaDownloadTest;
import google.registry.bsa.persistence.BsaLabelTest;
+import google.registry.bsa.persistence.BsaTldTest;
import google.registry.bsa.persistence.BsaUnblockableDomainTest;
import google.registry.model.billing.BillingBaseTest;
import google.registry.model.common.CursorTest;
@@ -77,6 +78,7 @@
BsaDomainRefreshTest.class,
BsaDownloadTest.class,
BsaLabelTest.class,
+ BsaTldTest.class,
BsaUnblockableDomainTest.class,
BulkPricingPackageTest.class,
ClaimsListDaoTest.class,
diff --git a/db/src/main/resources/sql/schema/db-schema.sql.generated b/db/src/main/resources/sql/schema/db-schema.sql.generated
index c9d74594eb9..d10fbb5f3fd 100644
--- a/db/src/main/resources/sql/schema/db-schema.sql.generated
+++ b/db/src/main/resources/sql/schema/db-schema.sql.generated
@@ -112,6 +112,12 @@
primary key (label)
);
+ create table "BsaTld" (
+ tld text not null,
+ ready_time timestamp(6) with time zone not null,
+ primary key (tld)
+ );
+
create table "BsaUnblockableDomain" (
label text not null,
tld text not null,