Courier MFT

Domain Model

Entities, value objects, enums, and relationships in Courier's domain model.

This section defines all entities in the Courier system, their relationships, and the conventions used across the codebase. The domain model serves as the single source of truth for the data structures that underpin every subsystem.

4.1 Design Conventions

Aggregate roots are implemented as C# class types. These are mutable entities tracked by EF Core's change tracker and represent the top-level objects that own their child entities. Examples: Job, Connection, PgpKey, FileMonitor.

Value objects are implemented as C# record types. These are immutable, compared by value, and typically serialized as JSON columns or embedded within aggregate roots. Examples: StepConfiguration, FailurePolicy, TransferProgress, VerifyResult.

General conventions:

  • All entities use Guid primary keys, generated application-side via Guid.NewGuid()
  • All entities include CreatedAt and UpdatedAt timestamps, set automatically via EF Core interceptors
  • All major entities support soft delete via an IsDeleted flag and DeletedAt timestamp. Soft-deleted entities are excluded from normal queries via a global query filter
  • Nullable reference types are enabled project-wide. Non-nullable properties are required; nullable properties are optional
  • Navigation properties use IReadOnlyList<T> for collections to enforce modification through aggregate root methods
  • JSON columns (PostgreSQL JSONB) are used for flexible, schema-light data like step configuration and audit details

4.2 Entity Catalog

4.2.1 Job System Entities

Job (aggregate root — class)

The central entity representing a named, versioned pipeline of steps.

PropertyTypeDescription
IdGuidPrimary key
NamestringHuman-readable job name
Descriptionstring?Optional description
CurrentVersionintLatest version number
FailurePolicyFailurePolicyValue object: policy, max retries, backoff config
IsEnabledboolWhether the job can be scheduled/triggered
CreatedAtDateTimeOffsetCreation timestamp
UpdatedAtDateTimeOffsetLast modification timestamp
IsDeletedboolSoft delete flag
DeletedAtDateTimeOffset?Soft delete timestamp
StepsIReadOnlyList<JobStep>Ordered list of step definitions
SchedulesIReadOnlyList<JobSchedule>Attached schedules
VersionsIReadOnlyList<JobVersion>Historical configuration snapshots
ExecutionsIReadOnlyList<JobExecution>Execution history
DependenciesIReadOnlyList<JobDependency>Upstream dependencies
TagsIReadOnlyList<EntityTag>Associated tags

JobStep (entity owned by Job)

PropertyTypeDescription
IdGuidPrimary key
JobIdGuidFK to parent Job
StepOrderintExecution order (0-based)
NamestringHuman-readable step name
TypeKeystringStep type identifier (e.g., sftp.download)
ConfigurationStepConfigurationValue object serialized as JSONB
TimeoutSecondsintStep timeout (default: 300)

JobVersion (entity owned by Job)

PropertyTypeDescription
IdGuidPrimary key
JobIdGuidFK to parent Job
VersionNumberintIncrementing version
ConfigSnapshotstringFull job configuration as JSON
CreatedAtDateTimeOffsetWhen this version was created
CreatedBystringUser who made the change

JobExecution (aggregate root — class)

A single run of a job. Owns its step executions and context snapshot.

PropertyTypeDescription
IdGuidPrimary key
JobIdGuidFK to Job
JobVersionNumberintWhich version was executed
ChainExecutionIdGuid?FK to ChainExecution if part of a chain
TriggeredBystring"schedule", "manual:{userId}", "monitor:{monitorId}"
StateJobExecutionStateEnum: Created, Queued, Running, Paused, Completed, Failed, Cancelled
QueuedAtDateTimeOffset?When the execution entered the queue
StartedAtDateTimeOffset?When execution began
CompletedAtDateTimeOffset?When execution finished
ContextSnapshotstringSerialized JobContext as JSON for checkpoint/resume
StepExecutionsIReadOnlyList<StepExecution>Per-step execution records

StepExecution (entity owned by JobExecution)

PropertyTypeDescription
IdGuidPrimary key
JobExecutionIdGuidFK to parent JobExecution
JobStepIdGuidFK to the JobStep definition
StepOrderintExecution order
StateStepExecutionStateEnum: Pending, Running, Completed, Failed, Skipped
StartedAtDateTimeOffset?When step began
CompletedAtDateTimeOffset?When step finished
DurationMslong?Execution duration in milliseconds
BytesProcessedlong?Total bytes transferred/processed
OutputDatastring?Step output written to JobContext, as JSON
ErrorMessagestring?Error message if failed
ErrorStackTracestring?Stack trace if failed
RetryAttemptintCurrent retry attempt number (0 = first try)

JobChain (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
NamestringHuman-readable chain name
Descriptionstring?Optional description
IsEnabledboolWhether the chain can be scheduled/triggered
CreatedAtDateTimeOffsetCreation timestamp
UpdatedAtDateTimeOffsetLast modification timestamp
IsDeletedboolSoft delete flag
DeletedAtDateTimeOffset?Soft delete timestamp
MembersIReadOnlyList<JobChainMember>Ordered job references with dependencies
SchedulesIReadOnlyList<ChainSchedule>Attached schedules
TagsIReadOnlyList<EntityTag>Associated tags

JobChainMember (entity owned by JobChain)

PropertyTypeDescription
IdGuidPrimary key
ChainIdGuidFK to parent JobChain
JobIdGuidFK to the Job
ExecutionOrderintOrder within the chain
DependsOnMemberIdGuid?FK to upstream JobChainMember (null = chain entry point)
RunOnUpstreamFailureboolWhether to run if upstream member fails (default: false)

ChainExecution (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
ChainIdGuidFK to JobChain
TriggeredBystring"schedule", "manual:{userId}"
StateChainExecutionStateEnum: Pending, Running, Completed, Failed, Paused, Cancelled
StartedAtDateTimeOffset?When chain execution began
CompletedAtDateTimeOffset?When chain execution finished
JobExecutionsIReadOnlyList<JobExecution>All job executions within this chain run

JobDependency (entity)

Represents a dependency edge between two standalone jobs (outside of chains).

PropertyTypeDescription
IdGuidPrimary key
UpstreamJobIdGuidFK to the job that must complete first
DownstreamJobIdGuidFK to the job that depends on the upstream
RunOnFailureboolAllow downstream to run even if upstream fails

JobSchedule (entity)

Attached to a Job via job_schedules table.

PropertyTypeDescription
IdGuidPrimary key
JobIdGuidFK to Job
ScheduleTypestring"cron" or "one_shot"
CronExpressionstring?Quartz cron expression (nullable for one-shot)
RunAtDateTimeOffset?One-shot execution time (nullable for cron)
IsEnabledboolWhether the schedule is active
LastFiredAtDateTimeOffset?Last time this schedule triggered
NextFireAtDateTimeOffset?Next calculated fire time

ChainSchedule (entity)

Attached to a JobChain via chain_schedules table. Mirrors JobSchedule exactly.

PropertyTypeDescription
IdGuidPrimary key
ChainIdGuidFK to JobChain
ScheduleTypestring"cron" or "one_shot"
CronExpressionstring?Quartz cron expression (nullable for one-shot)
RunAtDateTimeOffset?One-shot execution time (nullable for cron)
IsEnabledboolWhether the schedule is active
LastFiredAtDateTimeOffset?Last time this schedule triggered
NextFireAtDateTimeOffset?Next calculated fire time

4.2.2 Connection Entities

Connection (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
NamestringHuman-readable name
Groupstring?Organizational folder/group
ProtocolConnectionProtocolEnum: SFTP, FTP, FTPS
HoststringHostname or IP
PortintPort number
AuthMethodAuthMethodEnum: Password, SshKey, PasswordAndSshKey
UsernamestringLogin username
PasswordEncryptedbyte[]?AES-256 encrypted password
SshKeyIdGuid?FK to SshKey
HostKeyPolicyHostKeyPolicyEnum: TrustOnFirstUse, AlwaysTrust, Manual
StoredHostFingerprintstring?Known host key fingerprint
PassiveModeboolFTP/FTPS: passive mode (default: true)
TlsVersionFloorTlsVersion?FTPS: minimum TLS version
TlsCertPolicyTlsCertPolicyFTPS: cert validation mode (SystemTrust, PinnedThumbprint, Insecure)
TlsPinnedThumbprintstring?FTPS: expected SHA-256 cert thumbprint (when policy is PinnedThumbprint)
SshAlgorithmsSshAlgorithmConfig?Value object serialized as JSONB
ConnectTimeoutSecintConnection timeout (default: 30)
OperationTimeoutSecintPer-operation timeout (default: 300)
KeepaliveIntervalSecintKeepalive interval (default: 60)
TransportRetriesintAuto-reconnect attempts (default: 2)
StatusConnectionStatusEnum: Active, Disabled
FipsOverrideboolAllow non-FIPS algorithms for this connection
Notesstring?Free-text notes
CreatedAtDateTimeOffset
UpdatedAtDateTimeOffset
IsDeletedbool
DeletedAtDateTimeOffset?
TagsIReadOnlyList<EntityTag>Associated tags

KnownHost (entity owned by Connection)

PropertyTypeDescription
IdGuidPrimary key
ConnectionIdGuidFK to Connection
FingerprintstringSHA-256 fingerprint
KeyTypestringAlgorithm (e.g., ssh-rsa, ssh-ed25519)
FirstSeenDateTimeOffsetWhen first recorded
LastSeenDateTimeOffsetLast successful connection
ApprovedBystringUser or "system" for TOFU

4.2.3 Key Store Entities

PgpKey (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
NamestringHuman-readable label
FingerprintstringFull PGP fingerprint (40 hex chars)
ShortKeyIdstringShort key ID (16 hex chars)
AlgorithmPgpAlgorithmEnum: RSA_2048, RSA_3072, RSA_4096, ECC_CURVE25519, etc.
KeyTypePgpKeyTypeEnum: PublicOnly, KeyPair
Purposestring?Free-text notes
StatusPgpKeyStatusEnum: Active, Expiring, Retired, Revoked, Deleted
PublicKeyDatastringASCII-armored public key
PrivateKeyDatabyte[]?AES-256 encrypted private key (null for public-only)
PassphraseHashstring?Encrypted passphrase
ExpiresAtDateTimeOffset?Key expiration date
SuccessorKeyIdGuid?FK to replacement key (for rotation)
CreatedBystringUser who generated/imported
CreatedAtDateTimeOffset
UpdatedAtDateTimeOffset
IsDeletedbool
DeletedAtDateTimeOffset?
TagsIReadOnlyList<EntityTag>Associated tags

SshKey (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
NamestringHuman-readable label
KeyTypeSshKeyTypeEnum: RSA_2048, RSA_4096, ED25519, ECDSA_256
PublicKeyDatastringOpenSSH-format public key
PrivateKeyDatabyte[]AES-256 encrypted private key
PassphraseHashstring?Encrypted passphrase (nullable)
FingerprintstringSHA-256 fingerprint
StatusSshKeyStatusEnum: Active, Retired, Deleted
Notesstring?Free-text notes
CreatedBystringUser who generated/imported
CreatedAtDateTimeOffset
UpdatedAtDateTimeOffset
IsDeletedbool
DeletedAtDateTimeOffset?
TagsIReadOnlyList<EntityTag>Associated tags

4.2.4 File Monitor Entities

FileMonitor (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
NamestringHuman-readable name
Descriptionstring?Optional description
WatchTargetWatchTargetValue object: target type (Local/Remote), path, connection ID
TriggerEventsTriggerEventFlagsFlags enum: FileCreated, FileModified, FileExists
FilePatternsList<string>Glob patterns stored as JSONB array
PollingIntervalSecintPolling interval (default: 60)
StabilityWindowSecintFile readiness window (default: 5)
BatchModeboolTrue = batch, false = individual (default: true)
MaxConsecutiveFailuresintError threshold (default: 5)
ConsecutiveFailureCountintCurrent failure counter
StateMonitorStateEnum: Active, Paused, Disabled, Error
CreatedAtDateTimeOffset
UpdatedAtDateTimeOffset
IsDeletedbool
DeletedAtDateTimeOffset?
BoundJobsIReadOnlyList<MonitorJobBinding>Linked jobs/chains
TagsIReadOnlyList<EntityTag>Associated tags

MonitorJobBinding (entity owned by FileMonitor)

PropertyTypeDescription
IdGuidPrimary key
MonitorIdGuidFK to FileMonitor
JobIdGuid?FK to Job (nullable — set if bound to a job)
ChainIdGuid?FK to JobChain (nullable — set if bound to a chain)

MonitorDirectoryState (entity owned by FileMonitor)

PropertyTypeDescription
IdGuidPrimary key
MonitorIdGuidFK to FileMonitor
DirectoryListingstringJSON array of {path, size, lastModified}
CapturedAtDateTimeOffsetWhen this snapshot was taken

MonitorFileLog (entity owned by FileMonitor)

PropertyTypeDescription
IdGuidPrimary key
MonitorIdGuidFK to FileMonitor
FilePathstringFull path of detected file
FileSizelongSize in bytes at trigger time
FileHashstring?Optional SHA-256 hash
LastModifiedDateTimeOffsetFile's last modified timestamp
TriggeredAtDateTimeOffsetWhen the trigger fired
ExecutionIdGuidFK to the JobExecution created

4.2.5 Cross-Cutting Entities

Tag (aggregate root — class)

PropertyTypeDescription
IdGuidPrimary key
NamestringTag label (unique, case-insensitive)
Colorstring?Hex color code for UI display (e.g., #FF5733)
Categorystring?Grouping category (e.g., "Partner", "Environment")
Descriptionstring?Optional description
CreatedAtDateTimeOffset
IsDeletedbool
DeletedAtDateTimeOffset?

EntityTag (join entity)

Polymorphic association linking any taggable entity to a tag.

PropertyTypeDescription
IdGuidPrimary key
TagIdGuidFK to Tag
EntityTypeTaggableEntityTypeEnum: Job, JobChain, Connection, PgpKey, SshKey, FileMonitor
EntityIdGuidFK to the tagged entity (not a database FK — resolved in application)

AuditLogEntry (entity — append-only)

Unified audit log with an entity type discriminator.

PropertyTypeDescription
IdGuidPrimary key
EntityTypeAuditableEntityTypeEnum: Job, JobExecution, StepExecution, Chain, Connection, PgpKey, SshKey, FileMonitor
EntityIdGuidFK to the audited entity
OperationstringOperation name (e.g., StateChanged, UsedForEncrypt, Connected)
PerformedBystringUser ID or "system"
PerformedAtDateTimeOffsetTimestamp
DetailsstringJSONB: operation-specific context (old/new state, error details, bytes transferred, etc.)

The audit log uses a single table with a JSONB Details column for flexibility. Subsystem-specific fields (bytes transferred, transfer rate, error stack traces) are stored in the Details JSON rather than as top-level columns. This avoids a wide, sparse table while keeping all audit data queryable via PostgreSQL's JSONB operators.

Indexes: (EntityType, EntityId, PerformedAt) for entity-specific history queries, (PerformedAt) for time-range queries, (PerformedBy, PerformedAt) for user activity queries.

DomainEvent (entity — append-only)

Persisted domain events for V2 notification subscribers. Separate from the audit log because events are actionable (consumed by handlers) while audit entries are historical records.

PropertyTypeDescription
IdGuidPrimary key
EventTypestringEvent name (e.g., JobCompleted, KeyExpiringSoon)
EntityTypestringSource entity type
EntityIdGuidSource entity ID
PayloadstringJSONB: event-specific data
OccurredAtDateTimeOffsetWhen the event occurred
ProcessedAtDateTimeOffset?When a subscriber consumed the event (null = pending)
ProcessedBystring?Which subscriber processed it

In V1, events are written but ProcessedAt remains null (no subscribers yet). This table is designed as a transactional outbox: events are written in the same database transaction as the state change that produced them, guaranteeing consistency. In V2, this outbox becomes the foundation for event-driven scheduling — a relay process reads unprocessed events (using FOR UPDATE SKIP LOCKED to prevent duplicate delivery), publishes them to a message bus (Azure Service Bus or RabbitMQ), and marks them processed. This replaces database polling for job coordination and enables at-least-once delivery, fan-out notifications, and horizontal Worker scaling (see Section 15).

SystemSetting (entity)

Key-value configuration for runtime-adjustable settings.

PropertyTypeDescription
KeystringPrimary key (e.g., job.concurrency_limit)
ValuestringSetting value (parsed by application)
Descriptionstring?Human-readable description
UpdatedAtDateTimeOffsetLast modification timestamp
UpdatedBystringUser who changed the setting

4.3 Value Objects

Value objects are implemented as C# record types. They are immutable, compared by value, and stored either as JSONB columns or as embedded properties within their parent entity.

public record FailurePolicy(
    FailurePolicyType Type,       // Stop, RetryStep, RetryJob, SkipAndContinue
    int MaxRetries = 3,
    int BackoffBaseSeconds = 1,
    int BackoffMaxSeconds = 60);

public record StepConfiguration(
    Dictionary<string, object> Parameters);  // Flexible key-value config per step type

public record WatchTarget(
    WatchTargetType Type,         // Local, Remote
    string Path,                  // Directory path
    Guid? ConnectionId);          // FK to Connection (null for local)

public record SshAlgorithmConfig(
    List<string>? KeyExchange,
    List<string>? Encryption,
    List<string>? Mac,
    List<string>? HostKey);

public record SplitArchiveConfig(
    bool Enabled,
    int MaxPartSizeMb = 500);

4.4 Enumerations

// Job System
public enum JobExecutionState { Created, Queued, Running, Paused, Completed, Failed, Cancelled }
public enum StepExecutionState { Pending, Running, Completed, Failed, Skipped }
public enum ChainExecutionState { Pending, Running, Completed, Failed, Paused, Cancelled }
public enum FailurePolicyType { Stop, RetryStep, RetryJob, SkipAndContinue }
public enum ScheduleType { Cron, OneShot }

// Connections
public enum ConnectionProtocol { SFTP, FTP, FTPS }
public enum AuthMethod { Password, SshKey, PasswordAndSshKey }
public enum HostKeyPolicy { TrustOnFirstUse, AlwaysTrust, Manual }
public enum TlsCertPolicy { SystemTrust, PinnedThumbprint, Insecure }
public enum ConnectionStatus { Active, Disabled }
public enum TlsVersion { TLS_1_0, TLS_1_1, TLS_1_2, TLS_1_3 }

// Keys
public enum PgpAlgorithm { RSA_2048, RSA_3072, RSA_4096, ECC_CURVE25519, ECC_P256, ECC_P384 }
public enum PgpKeyType { PublicOnly, KeyPair }
public enum PgpKeyStatus { Active, Expiring, Retired, Revoked, Deleted }
public enum SshKeyType { RSA_2048, RSA_4096, ED25519, ECDSA_256 }
public enum SshKeyStatus { Active, Retired, Deleted }

// File Monitor
public enum MonitorState { Active, Paused, Disabled, Error }
public enum WatchTargetType { Local, Remote }

[Flags]
public enum TriggerEventFlags
{
    FileCreated = 1,
    FileModified = 2,
    FileExists = 4
}

// Cross-cutting
public enum TaggableEntityType { Job, JobChain, Connection, PgpKey, SshKey, FileMonitor }
public enum AuditableEntityType { Job, JobExecution, StepExecution, Chain, ChainExecution, Connection, PgpKey, SshKey, FileMonitor }

4.5 Entity Relationship Diagram

┌──────────────────────────────────────────────────────────────────────────────────┐
│                              JOB SYSTEM                                          │
│                                                                                  │
│  ┌─────────┐ 1    * ┌──────────┐         ┌──────────────┐                       │
│  │   Job   │───────│ JobStep  │         │ JobSchedule  │                       │
│  └────┬────┘        └──────────┘         └──────────────┘                       │
│       │ 1                                  1 *                                   │
│       ├──────────* ┌──────────────┐                                              │
│       │            │  JobVersion  │         ┌────────────┐  1  * ┌──────────────┐│
│       │            └──────────────┘         │ JobChain   │─────│ChainSchedule ││
│       │ 1                                    └────┬────┘       └──────────────┘│
│       ├──────────* ┌──────────────┐               │ 1                            │
│       │            │ JobExecution │◄──────────┐   ├──────* ┌────────────────┐    │
│       │            └──────┬───────┘           │   │        │ JobChainMember │    │
│       │                   │ 1                  │   │        └────────────────┘    │
│       │                   ├────* ┌─────────────┤   │ 1                            │
│       │                   │     │StepExecution ││   ├──────* ┌────────────────┐    │
│       │                   │     └──────────────┘│   │        │ChainExecution │    │
│       │                   │                      │   │        └────────────────┘    │
│       │ *                 └──────────────────────┘   │                              │
│       ├────────────── ┌────────────────┐              │                             │
│       │               │ JobDependency  │              │                             │
│       │               │ (upstream/     │              │                             │
│       │               │  downstream)   │              │                             │
│       │               └────────────────┘              │                             │
│       │                                               │                             │
│  ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  │
│       │              CONNECTIONS                      │                             │
│       │                                               │                             │
│       │         ┌────────────┐ 1    * ┌───────────┐  │                             │
│       │         │ Connection │───────│ KnownHost │  │                             │
│       │         └──────┬─────┘        └───────────┘  │                             │
│       │                │                              │                             │
│       │                │ *..1                          │                             │
│       │                ▼                               │                             │
│       │         ┌────────────┐                        │                             │
│       │         │   SshKey   │                        │                             │
│       │         └────────────┘                        │                             │
│       │                                               │                             │
│  ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  │
│       │              KEY STORE                        │                             │
│       │                                               │                             │
│       │         ┌────────────┐                        │                             │
│       │         │   PgpKey   │                        │                             │
│       │         │            │───── successor_key_id  │                             │
│       │         └────────────┘      (self-ref)        │                             │
│       │                                               │                             │
│  ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─  │
│       │            FILE MONITORS                      │                             │
│       │                                               │                             │
│       │  ┌─────────────┐ 1  * ┌────────────────────┐ │                             │
│       ├─│ FileMonitor  │────│ MonitorJobBinding   │─┘                             │
│       │  └──────┬──────┘     └────────────────────┘                               │
│       │         │ 1                                                                │
│       │         ├──────* ┌───────────────────────┐                                │
│       │         │        │ MonitorDirectoryState  │                                │
│       │         │        └───────────────────────┘                                │
│       │         │ 1                                                                │
│       │         └──────* ┌──────────────────┐                                     │
│       │                  │  MonitorFileLog   │                                     │
│       │                  └──────────────────┘                                     │
│       │                                                                            │
│  ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│       │              CROSS-CUTTING                                                 │
│       │                                                                            │
│       │  ┌─────────┐ *    * ┌───────────┐                                         │
│       └─│   Tag    │◄─────│ EntityTag  │──── polymorphic FK to any taggable entity│
│          └─────────┘        └───────────┘                                         │
│                                                                                    │
│          ┌────────────────┐     ┌──────────────┐     ┌───────────────┐            │
│          │ AuditLogEntry  │     │ DomainEvent  │     │ SystemSetting │            │
│          │ (unified,      │     │ (actionable, │     │ (key-value    │            │
│          │  append-only)  │     │  append-only) │     │  config)      │            │
│          └────────────────┘     └──────────────┘     └───────────────┘            │
│                                                                                    │
└──────────────────────────────────────────────────────────────────────────────────┘

KEY RELATIONSHIPS:
  Job ──1:*──► JobStep             Job owns its ordered steps
  Job ──1:*──► JobVersion          Job owns its version history
  Job ──1:*──► JobExecution        Job owns its execution history
  Job ──1:*──► JobSchedule         Job can have multiple schedules
  Job ──*:*──► Job                 Via JobDependency (upstream/downstream)
  JobExecution ──1:*──► StepExecution    Execution owns step executions
  JobChain ──1:*──► JobChainMember      Chain owns ordered member references
  JobChain ──1:*──► ChainExecution      Chain owns chain execution history
  JobChain ──1:*──► ChainSchedule       Chain can have multiple schedules
  ChainExecution ──1:*──► JobExecution   Chain execution owns job executions
  Connection ──1:*──► KnownHost         Connection owns host fingerprints
  Connection ──*:1──► SshKey            Connections reference SSH keys
  FileMonitor ──1:*──► MonitorJobBinding    Monitor binds to jobs/chains
  FileMonitor ──1:*──► MonitorDirectoryState Monitor owns directory snapshots
  FileMonitor ──1:*──► MonitorFileLog       Monitor owns dedup log
  MonitorJobBinding ──*:1──► Job/JobChain   Binding references a job or chain
  PgpKey ──0:1──► PgpKey            Successor key (self-referencing)
  Tag ──*:*──► (any taggable)       Via EntityTag polymorphic join