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
Browser / Drupal / MCP
│
▼
damd API ───── PostgreSQL
│ ├─ global registry
│ └─ schema per tenant
├────────── S3-compatible storage
└────────── durable jobs
│
▼
dam-worker
├─ verify + derive
├─ enrich + index
└─ tier + restore| Component | Owns | Does not own |
|---|---|---|
| damd | REST, MCP, authorization, delivery decisions | Long-running media work |
| dam-worker | Jobs, derivatives, indexing, lifecycle | Public HTTP traffic |
| PostgreSQL | Authoritative metadata and durable queue | Original media bytes |
| Object storage | Originals, derivatives and manifests | Authorization decisions |
| Tantivy + pgvector | Fast derived retrieval | Authoritative 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.