Skip to main content

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):

AttributeValue
eventTypesame as JSON eventType
tenantIdsame as JSON tenantId
schemaVersionstring '1'
traceparentW3C trace context
idempotencyKeysame as eventId
dataClassone of `public_media

2. Versioning rules

  • Additive changes (new optional field) → bump schemaVersion in JSON Schema, same topic version (.v1).
  • Breaking changes → publish a new version (.v2) on a new topic for ≥ 90 days alongside .v1. Mark .v1 deprecated in contracts/events/DEPRECATIONS.md.
  • All consumers must tolerate unknown additional fields (forward-compatible parsing).

3. Topics & subscriptions

TopicProducersSubscribers (initial)
melmastoon.file.upload.lifecycle.v1file-storage-servicebff-backoffice (progress UI), analytics
melmastoon.file.scan.results.v1file-storage-serviceproperty-service (photo readiness), notification-service, billing-service, theme-config-service, security siem
melmastoon.file.optimization.v1file-storage-serviceproperty-service, theme-config-service, search-aggregation-service (cover image refresh)
melmastoon.file.deletion.v1file-storage-serviceproperty-service, theme-config-service, billing-service (deferred), analytics
melmastoon.file.access.v1file-storage-servicesecurity siem, audit-archive
melmastoon.file.retention.v1file-storage-servicecompliance dashboard
melmastoon.file.erasure.v1file-storage-servicetenant-service (cert collation), reservation-service (cascade), audit-archive
melmastoon.file.quota.v1file-storage-servicetenant-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-service flips the corresponding Photo.status from uploaded → ready on 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_tenant events 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

EventSource serviceEffect
melmastoon.tenant.guest.erasure_requested.v1tenant-serviceenqueues EraseByGuestUseCase
melmastoon.tenant.deleted.v1tenant-serviceregisters retention hold; on release runs EraseByTenantUseCase
melmastoon.tenant.plan_changed.v1tenant-serviceupdates quotas.cap_bytes and cap_objects
melmastoon.tenant.settings.changed.v1tenant-servicerefreshes per-tenant retention policy overrides cache
melmastoon.property.photo.removed.v1property-servicesoft-deletes the underlying FileObject
melmastoon.billing.invoice.issued.v1billing-serviceapplies tax_compliance retention policy to the linked PDF
melmastoon.reservation.checked_out.v1reservation-servicemarks linked guest ID scans redaction_eligible per policy

13. Idempotency & ordering

  • Pub/Sub at-least-once; consumers MUST dedupe by eventId in their inbox table.
  • Ordering key tenant_id|fileObjectId guarantees 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 for main.