file-storage-service — Event Schemas
Companion: DOMAIN_MODEL · APPLICATION_LOGIC · 04 Event-Driven Architecture · Naming
All events follow melmastoon.<service>.<aggregate>.<verb_past>.v<n> and ship over GCP Pub/Sub with the transactional outbox pattern. The canonical short name for this service is file (not file_storage); this is the form used in topic names and event types throughout the platform. Ordering key is tenant_id|aggregate_id. Schemas live in services/file-storage-service/contracts/events/*.json (JSON Schema draft 2020-12) and are CI-validated against the example payloads in this document.
1. Envelope (shared across every Melmastoon event)
{
"eventId": "evt_01H...",
"eventType": "melmastoon.file.upload.completed.v1",
"schemaVersion": 1,
"occurredAt": "2026-04-22T08:24:11.123Z",
"tenantId": "tnt_01H...",
"aggregate": { "type": "file_object", "id": "med_01H..." },
"version": 2,
"producer": { "service": "file-storage-service", "version": "1.0.0" },
"trace": {
"traceId": "00f067aa0ba902b7",
"spanId": "00f067aa0ba902b8",
"causationId": "evt_01H...",
"correlationId":"req_01H..."
},
"data": { /* payload, schema-version-specific */ }
}
Pub/Sub attributes (mirrored from the JSON):
| Attribute | Value |
|---|---|
eventType | same as JSON eventType |
tenantId | same as JSON tenantId |
schemaVersion | string '1' |
traceparent | W3C trace context |
idempotencyKey | same as eventId |
dataClass | one of `public_media |
2. Versioning rules
- Additive changes (new optional field) → bump
schemaVersionin JSON Schema, same topic version (.v1). - Breaking changes → publish a new version (
.v2) on a new topic for ≥ 90 days alongside.v1. Mark.v1deprecated incontracts/events/DEPRECATIONS.md. - All consumers must tolerate unknown additional fields (forward-compatible parsing).
3. Topics & subscriptions
| Topic | Producers | Subscribers (initial) |
|---|---|---|
melmastoon.file.upload.lifecycle.v1 | file-storage-service | bff-backoffice (progress UI), analytics |
melmastoon.file.scan.results.v1 | file-storage-service | property-service (photo readiness), notification-service, billing-service, theme-config-service, security siem |
melmastoon.file.optimization.v1 | file-storage-service | property-service, theme-config-service, search-aggregation-service (cover image refresh) |
melmastoon.file.deletion.v1 | file-storage-service | property-service, theme-config-service, billing-service (deferred), analytics |
melmastoon.file.access.v1 | file-storage-service | security siem, audit-archive |
melmastoon.file.retention.v1 | file-storage-service | compliance dashboard |
melmastoon.file.erasure.v1 | file-storage-service | tenant-service (cert collation), reservation-service (cascade), audit-archive |
melmastoon.file.quota.v1 | file-storage-service | tenant-service (notification), bff-backoffice (banner) |
DLQ per subscription with 5 delivery attempts, exponential backoff (1s..60s).
4. Upload lifecycle events
4.1 melmastoon.file.upload.initiated.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"uploadSessionId": "ups_01HXY...",
"scope": "property_photo",
"dataClass": "public_media",
"contentType": "image/jpeg",
"declaredBytes": 384210,
"ownerScopeRefs": { "propertyId": "ppt_01H...", "photoSlot": "gallery" },
"actor": { "userId": "usr_01H...", "kind": "user" },
"expiresAt": "2026-04-22T08:24:11Z"
}
}
4.2 melmastoon.file.upload.completed.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"uploadSessionId": "ups_01HXY...",
"scope": "property_photo",
"contentType": "image/jpeg",
"bytes": 384210,
"sha256": "f3c1b2...",
"ownerScopeRefs": { "propertyId": "ppt_01H...", "photoSlot": "gallery" },
"alias": false,
"aliasOfFileObjectId": null
}
}
If the confirm step detected a duplicate, alias: true and aliasOfFileObjectId: "med_01HCANONICAL...". Consumers may treat the canonical id as the source of truth.
4.3 melmastoon.file.upload.failed.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"uploadSessionId": "ups_01HXY...",
"reason": "session_expired",
"detail": "no confirm received within 1h",
"scope": "property_photo"
}
}
reason ∈ { "session_expired", "aborted_by_caller", "hash_mismatch", "scope_byte_cap_exceeded", "provider_unavailable" }.
5. Scan events
5.1 melmastoon.file.scan.requested.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"scope": "property_photo",
"scanner": "clamav",
"requestedAt": "2026-04-22T08:24:12Z"
}
}
5.2 melmastoon.file.scan.passed.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"scanResultId": "scn_01H...",
"scope": "property_photo",
"scanner": "clamav",
"engineVersion": "1.3.1",
"definitionsVersion": "27192",
"scannedAt": "2026-04-22T08:24:43Z"
}
}
Consumer note.
property-serviceflips the correspondingPhoto.statusfromuploaded → readyon this event. Consumers MUST be idempotent on(fileObjectId, scanResultId).
5.3 melmastoon.file.scan.failed.v1 (file is now quarantined)
{
"data": {
"fileObjectId": "med_01HXY...",
"scanResultId": "scn_01H...",
"scope": "property_photo",
"scanner": "clamav",
"engineVersion": "1.3.1",
"definitionsVersion": "27192",
"scannedAt": "2026-04-22T08:24:43Z",
"verdict": "failed",
"threats": ["EICAR-Test-Signature"],
"quarantineUntil": "2026-05-22T08:24:43Z"
}
}
6. Optimization events
6.1 melmastoon.file.optimization.completed.v1
Emitted once when the last variant of the requested batch is ready.
{
"data": {
"fileObjectId": "med_01HXY...",
"scope": "property_photo",
"variants": [
{ "preset": "thumb", "objectKey": "tenants/tnt_01H.../property_photo/2026/04/22/med_01HXY..._thumb.webp", "contentType": "image/webp", "bytes": 18432, "widthPx": 320, "heightPx": 213 },
{ "preset": "hero", "objectKey": "tenants/tnt_01H.../property_photo/2026/04/22/med_01HXY..._hero.webp", "contentType": "image/webp", "bytes": 124210, "widthPx": 1280, "heightPx": 853 },
{ "preset": "full", "objectKey": "tenants/tnt_01H.../property_photo/2026/04/22/med_01HXY..._full.webp", "contentType": "image/webp", "bytes": 284210, "widthPx": 1920, "heightPx": 1280 },
{ "preset": "avif_hero","objectKey": "tenants/tnt_01H.../property_photo/2026/04/22/med_01HXY..._hero.avif", "contentType": "image/avif", "bytes": 92410, "widthPx": 1280, "heightPx": 853 }
],
"completedAt": "2026-04-22T08:25:01Z"
}
}
7. Deletion events
7.1 melmastoon.file.deleted.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"scope": "property_photo",
"soft": true,
"actor": { "userId": "usr_01H...", "kind": "user" },
"reason": "user_request",
"ownerScopeRefs": { "propertyId": "ppt_01H...", "photoSlot": "gallery" },
"purgeEligibleAt": "2026-05-22T09:00:00Z"
}
}
For hard purge events (retention or erasure) consumers receive file.retention.expired.v1 or file.erasure.completed.v1 instead.
8. Access events
8.1 melmastoon.file.access.denied.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"attemptedTenantId": "tnt_01HOTHER...",
"actor": { "userId": "usr_01H...", "kind": "user" },
"reason": "cross_tenant",
"callerIp": "203.0.113.7",
"callerUserAgent": "Mozilla/5.0 ...",
"occurredAt": "2026-04-22T10:00:00Z"
}
}
reason ∈ { "cross_tenant", "revoked_signature", "expired_signature", "scan_pending", "quarantined", "missing_role" }.
SIEM consumes this topic; ≥ 3
cross_tenantevents from the same actor within 5 min raises a security incident.
9. Retention events
9.1 melmastoon.file.retention.expired.v1
{
"data": {
"fileObjectId": "med_01HXY...",
"scope": "guest_id_scan",
"retentionPolicyName": "pii_id_scan",
"ownerScopeRefs": { "guestId": "gst_01H...", "reservationId": "rsv_01H..." },
"purgedAt": "2026-05-22T00:00:00Z",
"purgedBytes": 384210,
"cdnInvalidated": false
}
}
10. Erasure events
10.1 melmastoon.file.erasure.completed.v1
Emitted once per FileObject that was purged in an erasure run:
{
"data": {
"fileObjectId": "med_01HXY...",
"erasureRequestId": "ers_01H...",
"scope": "guest_id_scan",
"ownerScopeRefs": { "guestId": "gst_01H...", "reservationId": "rsv_01H..." },
"reason": "guest_request_gdpr",
"purgedAt": "2026-04-22T09:02:11Z",
"purgedBytes": 384210
}
}
A summary event on a separate topic — melmastoon.file.erasure.batch_completed.v1 — wraps a single request:
{
"data": {
"erasureRequestId": "ers_01H...",
"scope": { "kind": "guest", "guestId": "gst_01H..." },
"matchedObjects": 14,
"purgedObjects": 12,
"deferredObjects": 2,
"deferred": [ { "fileObjectId": "med_01H...", "policy": "tax_compliance", "releasedAt": "2033-04-22T00:00:00Z" } ],
"certificateSha256": "9a82c1...",
"completedAt": "2026-04-22T09:02:11Z"
}
}
11. Quota events
11.1 melmastoon.file.bucket.quota_warning.v1
{
"data": {
"tenantId": "tnt_01H...",
"thresholdPct": 80,
"bytesUsed": "42949672960",
"bytesCap": "53687091200",
"objectsUsed": 18432,
"objectsCap": 100000,
"byScope": { "property_photo": { "bytes": "18253611008", "objects": 14210 } },
"raisedAt": "2026-04-22T11:00:00Z"
}
}
Subsequent thresholdPct=95 event dampens for 1 h before re-firing.
12. Consumed events
| Event | Source service | Effect |
|---|---|---|
melmastoon.tenant.guest.erasure_requested.v1 | tenant-service | enqueues EraseByGuestUseCase |
melmastoon.tenant.deleted.v1 | tenant-service | registers retention hold; on release runs EraseByTenantUseCase |
melmastoon.tenant.plan_changed.v1 | tenant-service | updates quotas.cap_bytes and cap_objects |
melmastoon.tenant.settings.changed.v1 | tenant-service | refreshes per-tenant retention policy overrides cache |
melmastoon.property.photo.removed.v1 | property-service | soft-deletes the underlying FileObject |
melmastoon.billing.invoice.issued.v1 | billing-service | applies tax_compliance retention policy to the linked PDF |
melmastoon.reservation.checked_out.v1 | reservation-service | marks linked guest ID scans redaction_eligible per policy |
13. Idempotency & ordering
- Pub/Sub at-least-once; consumers MUST dedupe by
eventIdin their inbox table. - Ordering key
tenant_id|fileObjectIdguarantees per-aggregate FIFO. - Out-of-order delivery between aggregates is permitted; consumers must reconcile via state, not order.
14. Schema registry & CI
- All event payloads have a JSON Schema in
contracts/events/<topic>.json. - CI runs:
validate-examples: every example in this doc must validate against its schema.compatibility-check: new schema versions must be backward-compatible (additive) within a.vN.pact-broker-publish: schemas are pushed to the Pact broker on green CI formain.