diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/AssemblyMarker.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/AssemblyMarker.cs
deleted file mode 100644
index 53d84d4..0000000
--- a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/AssemblyMarker.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace ZB.MOM.WW.Secrets.Abstractions;
-
-///
-/// Internal marker giving the assembly a compilable input while the contract
-/// surface is scaffolded. Replace with real contracts as they are implemented.
-///
-internal static class AssemblyMarker
-{
-}
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretContentType.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretContentType.cs
new file mode 100644
index 0000000..0002f17
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretContentType.cs
@@ -0,0 +1,20 @@
+namespace ZB.MOM.WW.Secrets.Abstractions;
+
+///
+/// Describes how the plaintext of a secret should be interpreted by consumers. This
+/// is metadata only — the stored ciphertext is opaque bytes regardless of content type.
+///
+public enum SecretContentType
+{
+ /// Arbitrary UTF-8 text (the default).
+ Text,
+
+ /// A database or service connection string.
+ ConnectionString,
+
+ /// A JSON document.
+ Json,
+
+ /// Binary data carried as a Base64-encoded string.
+ BinaryBase64,
+}
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretManifestEntry.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretManifestEntry.cs
new file mode 100644
index 0000000..2504f04
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretManifestEntry.cs
@@ -0,0 +1,21 @@
+namespace ZB.MOM.WW.Secrets.Abstractions;
+
+///
+/// A compact per-secret manifest row used for cluster anti-entropy: it carries just
+/// enough state (name, revision, tombstone flag, timestamp) for two nodes to compare
+/// inventories and reconcile which secrets are newer without exchanging ciphertext.
+///
+public record SecretManifestEntry
+{
+ /// The normalized secret name.
+ public required SecretName Name { get; init; }
+
+ /// Monotonic revision number used to determine which side is newer.
+ public required long Revision { get; init; }
+
+ /// When the secret was last updated (UTC).
+ public required DateTimeOffset UpdatedUtc { get; init; }
+
+ /// Whether the secret is soft-deleted (tombstoned).
+ public bool IsDeleted { get; init; }
+}
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretMetadata.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretMetadata.cs
new file mode 100644
index 0000000..54bef56
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretMetadata.cs
@@ -0,0 +1,39 @@
+namespace ZB.MOM.WW.Secrets.Abstractions;
+
+///
+/// The safe, UI-facing projection of a secret. Deliberately carries no
+/// array members, so neither ciphertext nor plaintext can leak into
+/// rendered markup or logs via this type — it is safe to bind directly in management UIs.
+///
+public record SecretMetadata
+{
+ /// The normalized secret name.
+ public required SecretName Name { get; init; }
+
+ /// Optional human-readable description.
+ public string? Description { get; init; }
+
+ /// How the decrypted plaintext should be interpreted.
+ public required SecretContentType ContentType { get; init; }
+
+ /// Identifier of the key-encryption key (KEK) that wrapped the DEK.
+ public required string KekId { get; init; }
+
+ /// Monotonic revision number.
+ public required long Revision { get; init; }
+
+ /// Whether the secret is soft-deleted (tombstoned).
+ public bool IsDeleted { get; init; }
+
+ /// When the secret was first created (UTC).
+ public required DateTimeOffset CreatedUtc { get; init; }
+
+ /// When the secret was last updated (UTC).
+ public required DateTimeOffset UpdatedUtc { get; init; }
+
+ /// Principal that created the secret, if known.
+ public string? CreatedBy { get; init; }
+
+ /// Principal that last updated the secret, if known.
+ public string? UpdatedBy { get; init; }
+}
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretName.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretName.cs
new file mode 100644
index 0000000..3eef4f7
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretName.cs
@@ -0,0 +1,63 @@
+namespace ZB.MOM.WW.Secrets.Abstractions;
+
+///
+/// A validated, normalized secret name. Names are trimmed, lowercased (invariant
+/// culture), and constrained to a filesystem-safe allow-list so they can double as
+/// path segments and stable identifiers. Normalization happens once, at construction.
+///
+public readonly record struct SecretName
+{
+ private const string AllowedChars = "abcdefghijklmnopqrstuvwxyz0123456789._/-";
+
+ ///
+ /// Creates a from the given raw value, normalizing then
+ /// validating it.
+ ///
+ /// The raw name to normalize and validate.
+ ///
+ /// The value is null, empty, or whitespace; contains the path-traversal sequence
+ /// ..; or contains any character outside [a-z0-9._/-] (after trim and
+ /// lowercasing).
+ ///
+ public SecretName(string value)
+ {
+ string normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
+
+ if (string.IsNullOrWhiteSpace(normalized))
+ {
+ throw new ArgumentException(
+ $"Secret name must not be null, empty, or whitespace (was '{value}').",
+ nameof(value));
+ }
+
+ if (normalized.Contains("..", StringComparison.Ordinal))
+ {
+ throw new ArgumentException(
+ $"Secret name must not contain the path-traversal sequence '..' (was '{value}').",
+ nameof(value));
+ }
+
+ foreach (char c in normalized)
+ {
+ if (!AllowedChars.Contains(c, StringComparison.Ordinal))
+ {
+ throw new ArgumentException(
+ $"Secret name contains disallowed character '{c}'; allowed characters are [a-z0-9._/-] (was '{value}').",
+ nameof(value));
+ }
+ }
+
+ Value = normalized;
+ }
+
+ /// The normalized secret name (trimmed, lowercased, allow-list validated).
+ public string Value { get; }
+
+ /// Returns the normalized .
+ /// The normalized secret name.
+ public override string ToString() => Value;
+
+ /// Implicitly converts a to its underlying string value.
+ /// The secret name to convert.
+ public static implicit operator string(SecretName name) => name.Value;
+}
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretsExceptions.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretsExceptions.cs
new file mode 100644
index 0000000..f1d1869
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/SecretsExceptions.cs
@@ -0,0 +1,13 @@
+namespace ZB.MOM.WW.Secrets.Abstractions;
+
+/// Thrown when the master key (KEK) required to wrap or unwrap a DEK is unavailable.
+public sealed class MasterKeyUnavailableException(string message) : InvalidOperationException(message);
+
+/// Thrown when a secret's ciphertext cannot be decrypted or its authentication tag fails to verify.
+public sealed class SecretDecryptionException(string message) : InvalidOperationException(message);
+
+/// Thrown when a requested secret does not exist (or is tombstoned) in the store.
+public sealed class SecretNotFoundException(string message) : InvalidOperationException(message);
+
+/// Thrown when the secret store cannot be migrated to the supported schema.
+public sealed class SecretStoreMigrationException(string message) : InvalidOperationException(message);
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/StoredSecret.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/StoredSecret.cs
new file mode 100644
index 0000000..28e8fc8
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/StoredSecret.cs
@@ -0,0 +1,61 @@
+namespace ZB.MOM.WW.Secrets.Abstractions;
+
+///
+/// The encrypted, at-rest representation of a secret — one database row. This type
+/// never holds plaintext: the secret material lives only in ,
+/// sealed under AES-256-GCM with a per-secret data-encryption key (DEK) that is itself
+/// wrapped by the key-encryption key (KEK) identified by .
+///
+public record StoredSecret
+{
+ /// The normalized secret name (primary identity).
+ public required SecretName Name { get; init; }
+
+ /// Optional human-readable description shown in management surfaces.
+ public string? Description { get; init; }
+
+ /// How the decrypted plaintext should be interpreted.
+ public required SecretContentType ContentType { get; init; }
+
+ /// The AES-256-GCM ciphertext of the secret plaintext.
+ public required byte[] Ciphertext { get; init; }
+
+ /// The AES-256-GCM nonce used to seal .
+ public required byte[] Nonce { get; init; }
+
+ /// The AES-256-GCM authentication tag for .
+ public required byte[] Tag { get; init; }
+
+ /// The per-secret data-encryption key (DEK), wrapped by the KEK.
+ public required byte[] WrappedDek { get; init; }
+
+ /// The AES-256-GCM nonce used to wrap .
+ public required byte[] WrapNonce { get; init; }
+
+ /// The AES-256-GCM authentication tag for .
+ public required byte[] WrapTag { get; init; }
+
+ /// Identifier of the key-encryption key (KEK) that wrapped the DEK.
+ public required string KekId { get; init; }
+
+ /// Monotonic revision number, incremented on each update.
+ public required long Revision { get; init; }
+
+ /// Whether the secret is soft-deleted (tombstoned).
+ public bool IsDeleted { get; init; }
+
+ /// When the secret was soft-deleted, if it is deleted; otherwise null.
+ public DateTimeOffset? DeletedUtc { get; init; }
+
+ /// When the secret was first created (UTC).
+ public required DateTimeOffset CreatedUtc { get; init; }
+
+ /// When the secret was last updated (UTC).
+ public required DateTimeOffset UpdatedUtc { get; init; }
+
+ /// Principal that created the secret, if known.
+ public string? CreatedBy { get; init; }
+
+ /// Principal that last updated the secret, if known.
+ public string? UpdatedBy { get; init; }
+}
diff --git a/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/SecretNameTests.cs b/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/SecretNameTests.cs
new file mode 100644
index 0000000..964f783
--- /dev/null
+++ b/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/SecretNameTests.cs
@@ -0,0 +1,21 @@
+using ZB.MOM.WW.Secrets.Abstractions;
+using Xunit;
+
+namespace ZB.MOM.WW.Secrets.Tests;
+
+public class SecretNameTests
+{
+ [Theory]
+ [InlineData("sql/historiangw/historian-password", "sql/historiangw/historian-password")]
+ [InlineData(" SQL/Foo ", "sql/foo")] // trimmed + lowercased
+ public void Normalizes(string input, string expected)
+ => Assert.Equal(expected, new SecretName(input).Value);
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("bad name with spaces")]
+ [InlineData("../escape")]
+ public void RejectsInvalid(string input)
+ => Assert.Throws(() => new SecretName(input));
+}