The load-bearing decisions

  • Rights are enforced at distribution. A signed URL permits an attempt; current evidence still decides whether bytes leave.
  • Search never touches the original blob. Metadata, text, vectors and generous proxies are extracted while bytes are hot.
  • Each tenant is a PostgreSQL schema. Requests enter through a transaction-scoped connection with its search path fixed.
  • Search is derived state. PostgreSQL is the record; Tantivy and pgvector can be rebuilt.
  • Processes split by failure profile. The API stays latency-sensitive while workers own CPU-heavy and untrusted media work.

Runtime topology

Processes and state text
Browser / Drupal / MCP
          │
          ▼
       damd API ───── PostgreSQL
          │             ├─ global registry
          │             └─ schema per tenant
          ├────────── S3-compatible storage
          └────────── durable jobs
                           │
                           ▼
                      dam-worker
                      ├─ verify + derive
                      ├─ enrich + index
                      └─ tier + restore
ComponentOwnsDoes not own
damdREST, MCP, authorization, delivery decisionsLong-running media work
dam-workerJobs, derivatives, indexing, lifecyclePublic HTTP traffic
PostgreSQLAuthoritative metadata and durable queueOriginal media bytes
Object storageOriginals, derivatives and manifestsAuthorization decisions
Tantivy + pgvectorFast derived retrievalAuthoritative state

The tenant boundary

Tenant data lives in schemas such as t_acme alongside the dam_global control plane. Application repositories receive a TenantConn, not a general pool handle.

The request opens a transaction and sets search_path for its duration. This makes the safe query the ordinary query: repository SQL does not need a tenant predicate that a future edit can forget.

Dependency direction

dam-core contains domain types and depends on no internal crate. Storage, database, media and search build on it. dam-api and dam-mcp sit at the top and do not depend on each other.

That shape keeps policy in shared domain code rather than allowing the REST and agent surfaces to produce different answers.