diff --git a/src/Weasel.Core/MultiTenancy/ExplicitTenantAssignment.cs b/src/Weasel.Core/MultiTenancy/ExplicitTenantAssignment.cs
new file mode 100644
index 0000000..90e3001
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/ExplicitTenantAssignment.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using JasperFx.MultiTenancy;
+
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Assignment strategy that always throws for unrecognized tenants.
+/// Requires all tenants to be explicitly pre-assigned to a database
+/// via the admin API before first use.
+///
+public class ExplicitTenantAssignment : ITenantAssignmentStrategy
+{
+ public ValueTask AssignTenantToDatabaseAsync(
+ string tenantId, IReadOnlyList availableDatabases)
+ {
+ throw new UnknownTenantIdException(tenantId);
+ }
+}
diff --git a/src/Weasel.Core/MultiTenancy/HashTenantAssignment.cs b/src/Weasel.Core/MultiTenancy/HashTenantAssignment.cs
new file mode 100644
index 0000000..d9518bf
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/HashTenantAssignment.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Assigns tenants to databases using a deterministic hash of the tenant ID.
+/// Uses a stable FNV-1a hash for consistent, well-distributed assignment.
+///
+public class HashTenantAssignment : ITenantAssignmentStrategy
+{
+ public ValueTask AssignTenantToDatabaseAsync(
+ string tenantId, IReadOnlyList availableDatabases)
+ {
+ if (availableDatabases.Count == 0)
+ {
+ throw new InvalidOperationException(
+ "No available (non-full) databases in the pool to assign tenant to");
+ }
+
+ var hash = StableHash(tenantId);
+ var index = (int)(hash % (uint)availableDatabases.Count);
+
+ return new ValueTask(availableDatabases[index].DatabaseId);
+ }
+
+ ///
+ /// FNV-1a 32-bit hash — deterministic, fast, no external dependencies
+ ///
+ internal static uint StableHash(string value)
+ {
+ unchecked
+ {
+ uint hash = 2166136261;
+ foreach (var c in value)
+ {
+ hash ^= c;
+ hash *= 16777619;
+ }
+ return hash;
+ }
+ }
+}
diff --git a/src/Weasel.Core/MultiTenancy/IDatabaseSizingStrategy.cs b/src/Weasel.Core/MultiTenancy/IDatabaseSizingStrategy.cs
new file mode 100644
index 0000000..cdb081e
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/IDatabaseSizingStrategy.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Pluggable strategy for determining which database in a pool is the "smallest"
+/// and should receive the next tenant assignment. The default implementation
+/// uses tenant count, but implementations could query actual row counts,
+/// disk usage, or other metrics.
+///
+public interface IDatabaseSizingStrategy
+{
+ ///
+ /// Find the smallest database from the available (non-full) databases
+ ///
+ ValueTask FindSmallestDatabaseAsync(IReadOnlyList databases);
+}
diff --git a/src/Weasel.Core/MultiTenancy/ITenantAssignmentStrategy.cs b/src/Weasel.Core/MultiTenancy/ITenantAssignmentStrategy.cs
new file mode 100644
index 0000000..41e19ff
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/ITenantAssignmentStrategy.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Strategy for determining which database a new tenant should be assigned to.
+/// Implementations are called under an advisory lock, so they do not need to
+/// handle concurrency themselves.
+///
+public interface ITenantAssignmentStrategy
+{
+ ///
+ /// Determine which database a new tenant should be assigned to.
+ ///
+ /// The tenant being assigned
+ /// Non-full databases in the pool
+ /// The database_id to assign the tenant to
+ ///
+ /// Thrown if no suitable database is available
+ ///
+ ValueTask AssignTenantToDatabaseAsync(
+ string tenantId, IReadOnlyList availableDatabases);
+}
diff --git a/src/Weasel.Core/MultiTenancy/ITenantDatabasePool.cs b/src/Weasel.Core/MultiTenancy/ITenantDatabasePool.cs
new file mode 100644
index 0000000..2b3c0ca
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/ITenantDatabasePool.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Abstraction for managing a pool of databases used in sharded multi-tenancy.
+/// Implementations handle the persistence of database registry and tenant assignments.
+///
+public interface ITenantDatabasePool
+{
+ ///
+ /// List all databases in the pool with their current state
+ ///
+ ValueTask> ListDatabasesAsync(CancellationToken ct);
+
+ ///
+ /// Add a new database to the pool
+ ///
+ ValueTask AddDatabaseAsync(string databaseId, string connectionString, CancellationToken ct);
+
+ ///
+ /// Mark a database as full so no new tenants are assigned to it
+ ///
+ ValueTask MarkDatabaseFullAsync(string databaseId, CancellationToken ct);
+
+ ///
+ /// Find which database a tenant is assigned to, or null if not yet assigned
+ ///
+ ValueTask FindDatabaseForTenantAsync(string tenantId, CancellationToken ct);
+
+ ///
+ /// Assign a tenant to a specific database. Does not create partitions —
+ /// that is the responsibility of the caller (e.g., Marten's ShardedTenancy).
+ ///
+ ValueTask AssignTenantAsync(string tenantId, string databaseId, CancellationToken ct);
+
+ ///
+ /// Remove a tenant assignment
+ ///
+ ValueTask RemoveTenantAsync(string tenantId, CancellationToken ct);
+}
diff --git a/src/Weasel.Core/MultiTenancy/PooledDatabase.cs b/src/Weasel.Core/MultiTenancy/PooledDatabase.cs
new file mode 100644
index 0000000..9b5cd94
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/PooledDatabase.cs
@@ -0,0 +1,11 @@
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Represents a database in the sharded tenancy pool
+///
+public record PooledDatabase(
+ string DatabaseId,
+ string ConnectionString,
+ bool IsFull,
+ int TenantCount
+);
diff --git a/src/Weasel.Core/MultiTenancy/SmallestTenantAssignment.cs b/src/Weasel.Core/MultiTenancy/SmallestTenantAssignment.cs
new file mode 100644
index 0000000..3c8d8e5
--- /dev/null
+++ b/src/Weasel.Core/MultiTenancy/SmallestTenantAssignment.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Weasel.Core.MultiTenancy;
+
+///
+/// Assigns tenants to the database with the fewest tenants (or custom sizing metric).
+/// Uses a pluggable for determining "smallest".
+/// Defaults to sorting by .
+///
+public class SmallestTenantAssignment : ITenantAssignmentStrategy
+{
+ private readonly IDatabaseSizingStrategy _sizingStrategy;
+
+ public SmallestTenantAssignment(IDatabaseSizingStrategy? sizingStrategy = null)
+ {
+ _sizingStrategy = sizingStrategy ?? new TenantCountSizingStrategy();
+ }
+
+ public ValueTask AssignTenantToDatabaseAsync(
+ string tenantId, IReadOnlyList availableDatabases)
+ {
+ if (availableDatabases.Count == 0)
+ {
+ throw new InvalidOperationException(
+ "No available (non-full) databases in the pool to assign tenant to");
+ }
+
+ return _sizingStrategy.FindSmallestDatabaseAsync(availableDatabases);
+ }
+}
+
+///
+/// Default sizing strategy that picks the database with the lowest tenant count.
+///
+public class TenantCountSizingStrategy : IDatabaseSizingStrategy
+{
+ public ValueTask FindSmallestDatabaseAsync(IReadOnlyList databases)
+ {
+ var smallest = databases.OrderBy(d => d.TenantCount).First();
+ return new ValueTask(smallest.DatabaseId);
+ }
+}
diff --git a/src/Weasel.Postgresql/Tables/DatabasePoolTable.cs b/src/Weasel.Postgresql/Tables/DatabasePoolTable.cs
new file mode 100644
index 0000000..e52df8e
--- /dev/null
+++ b/src/Weasel.Postgresql/Tables/DatabasePoolTable.cs
@@ -0,0 +1,21 @@
+using Weasel.Core;
+
+namespace Weasel.Postgresql.Tables;
+
+///
+/// Schema definition for the mt_database_pool table used by sharded multi-tenancy.
+/// Tracks the available databases in the pool with their capacity status.
+///
+public class DatabasePoolTable : Table
+{
+ public const string TableName = "mt_database_pool";
+
+ public DatabasePoolTable(string schemaName)
+ : base(new DbObjectName(schemaName, TableName))
+ {
+ AddColumn("database_id").AsPrimaryKey();
+ AddColumn("connection_string").NotNull();
+ AddColumn("is_full").NotNull().DefaultValueByExpression("false");
+ AddColumn("tenant_count").NotNull().DefaultValue(0);
+ }
+}
diff --git a/src/Weasel.Postgresql/Tables/TenantAssignmentTable.cs b/src/Weasel.Postgresql/Tables/TenantAssignmentTable.cs
new file mode 100644
index 0000000..b969403
--- /dev/null
+++ b/src/Weasel.Postgresql/Tables/TenantAssignmentTable.cs
@@ -0,0 +1,28 @@
+using Weasel.Core;
+
+namespace Weasel.Postgresql.Tables;
+
+///
+/// Schema definition for the mt_tenant_assignments table used by sharded multi-tenancy.
+/// Maps tenant IDs to their assigned database in the pool.
+///
+public class TenantAssignmentTable : Table
+{
+ public const string TableName = "mt_tenant_assignments";
+
+ public TenantAssignmentTable(string schemaName)
+ : base(new DbObjectName(schemaName, TableName))
+ {
+ AddColumn("tenant_id").AsPrimaryKey();
+ AddColumn("database_id").NotNull();
+ AddColumn("assigned_at", "timestamptz").NotNull().DefaultValueByExpression("now()");
+
+ // Foreign key to the database pool table
+ ForeignKeys.Add(new ForeignKey("fk_tenant_assignment_database")
+ {
+ LinkedTable = new DbObjectName(schemaName, DatabasePoolTable.TableName),
+ ColumnNames = new[] { "database_id" },
+ LinkedNames = new[] { "database_id" }
+ });
+ }
+}