Use Cases & Business Scenarios
How InfraHub Storage powers real enterprise workflows — from invoice archival and HR document management to AI model artefacts, compliance evidence, multi-tenant isolation, and beyond.
AddObjectStorage() is registered in Program.cs. Each example injects IObjectStorageProvider — the same interface regardless of whether you are running on MinIO, AWS S3, Cloudflare R2, or Backblaze B2. No scenario requires knowledge of the underlying backend.
Scenarios
Scenario 1 Invoice & Accounts Payable Archival
Business Context
Finance teams are legally required to retain every generated invoice as an immutable, auditable record for seven or more years depending on jurisdiction. These documents must be retrievable on demand — for audits, dispute resolution, or customer requests — and must not be alterable after the fact. Traditional file systems or database BLOBs cannot easily guarantee immutability; InfraHub Storage's CID-based design does.
The CID is a SHA-256 hash of the file content. Any modification to the file would produce a different CID, making silent tampering detectable. Storing the CID alongside the invoice record in the database creates a cryptographic link between the database row and the stored file.
Implementation
public class InvoiceService
{
private readonly IObjectStorageProvider _storage;
private readonly IInvoiceRepository _repo;
public InvoiceService(IObjectStorageProvider storage, IInvoiceRepository repo)
{
_storage = storage;
_repo = repo;
}
public async Task<Invoice> GenerateAndArchiveAsync(InvoiceRequest req, CancellationToken ct)
{
// 1. Generate the PDF bytes (your existing PDF generation logic)
byte[] pdfBytes = await _pdfGenerator.RenderInvoicePdfAsync(req, ct);
// 2. Store in InfraHub Storage — returns a CID (SHA-256 hex string)
string cid = await _storage.StoreAsync(pdfBytes, "application/pdf", null, ct);
// 3. Persist the CID alongside the invoice record in the database
var invoice = new Invoice
{
InvoiceNumber = req.InvoiceNumber,
IssuedOn = DateTime.UtcNow,
TotalAmount = req.TotalAmount,
PdfCid = cid // link to immutable storage
};
await _repo.SaveAsync(invoice, ct);
return invoice;
}
public async Task<Stream> RetrieveInvoicePdfAsync(string cid, CancellationToken ct)
{
// Retrieve the file at any point — even 7 years later
return await _storage.GetStreamAsync(cid, ct);
}
}
Scenario 2 HR Document Management
Business Context
HR departments handle a high volume of sensitive, personally identifiable documents: employment contracts, passport copies, tax forms, offer letters, and onboarding packs. These documents must be stored securely, accessible only to the appropriate tenant, and — in multi-entity businesses — never mixed across companies. They may also be subject to right-to-erasure requests under GDPR, requiring a clean way to reference and delete specific employee documents.
With PrependTenantID: true, InfraHub Storage automatically prefixes every key with tenant_{tenantID}/. This means HR documents uploaded for Company A can never be retrieved by Company B, even though they share the same bucket — the key namespacing enforces isolation at the storage layer, not just in application logic.
Implementation
public class HrDocumentService
{
private readonly IObjectStorageProvider _storage;
private readonly IEmployeeDocumentRepository _repo;
public async Task<EmployeeDocument> UploadDocumentAsync(
int employeeID,
string documentType, // e.g. "Contract", "Passport", "TaxForm"
Stream fileStream,
string contentType,
CancellationToken ct)
{
byte[] bytes = await fileStream.ReadAllBytesAsync(ct);
// TenantID is resolved automatically from IAiSessionContext —
// the key stored in MinIO will be: tenant_42/a3f9b2c1...
string cid = await _storage.StoreAsync(bytes, contentType, null, ct);
var doc = new EmployeeDocument
{
EmployeeID = employeeID,
DocumentType = documentType,
FileCid = cid,
UploadedOn = DateTime.UtcNow,
Version = await _repo.GetNextVersionAsync(employeeID, documentType, ct)
};
await _repo.SaveAsync(doc, ct);
return doc;
}
public async Task<IReadOnlyList<EmployeeDocument>> GetDocumentHistoryAsync(
int employeeID, string documentType, CancellationToken ct)
{
// All versions — each upload creates a new CID, old CIDs are never overwritten
return await _repo.GetAllVersionsAsync(employeeID, documentType, ct);
}
}
Scenario 3 AI Model Artefacts & Knowledge Base
Business Context
Octopus AI agents generate and consume expensive computational artefacts: embedding vectors, model snapshots, knowledge base exports, and pre-computed retrieval indexes. Regenerating these artefacts on every run is computationally wasteful and slow. Persisting them in InfraHub Storage allows agents to check for the existence of a pre-computed artefact before triggering expensive generation — a form of content-addressed caching.
Because artefacts are identified by the SHA-256 of their content, the same model trained on the same dataset always produces the same CID. This enables deterministic cache hits across agent runs, deployments, and even tenants (when content is shared).
Implementation
public class KnowledgeBaseExportService
{
private readonly IObjectStorageProvider _storage;
private readonly IEmbeddingService _embedder;
/// <summary>
/// Returns the CID of the serialised knowledge snapshot.
/// If an identical snapshot already exists, returns immediately — no re-upload.
/// </summary>
public async Task<string> ExportSnapshotAsync(KnowledgeBase kb, CancellationToken ct)
{
byte[] snapshotBytes = await _serialiser.SerialiseAsync(kb, ct);
// StoreAsync is idempotent — identical bytes return the same CID
string cid = await _storage.StoreAsync(
snapshotBytes,
"application/octet-stream",
null,
ct);
return cid;
}
/// <summary>
/// Check-before-compute pattern: only generate embeddings if not already stored.
/// </summary>
public async Task<byte[]> GetOrComputeEmbeddingAsync(
string knownCid, Func<Task<byte[]>> computeFn, CancellationToken ct)
{
if (await _storage.ExistsAsync(knownCid, ct))
{
// Cache hit — retrieve without recomputing
using var stream = await _storage.GetStreamAsync(knownCid, ct);
return await stream.ReadAllBytesAsync(ct);
}
// Cache miss — compute and store for future calls
byte[] computed = await computeFn();
await _storage.StoreAsync(computed, "application/octet-stream", null, ct);
return computed;
}
}
ExistsAsync check before recomputing is a first-class pattern for AI workloads. Embedding generation for large corpora can take minutes; storing the result and checking existence before recomputing reduces agent execution time significantly for repeated tasks.
Scenario 4 Workflow Attachment Pipeline
Business Context
Approval workflows frequently require supporting evidence — expense receipts, signed forms, photos of damage, purchase orders. In a Flow Studio pipeline, a user uploads a file at the start of the workflow; that file needs to be accessible to every subsequent node without being re-uploaded or embedded in the workflow state as raw bytes (which would bloat the workflow context). InfraHub Storage solves this by returning a CID that is lightweight enough to pass as a workflow variable.
The CID travels through the pipeline as a string. Any node downstream can call GetStreamAsync to retrieve the full file — for PDF rendering, email attachment, virus scanning, OCR, or any other processing step.
Pipeline Flow
HTTP POST
Returns CID
Workflow state
Human review
Retrieve file
Downstream use
Implementation
// Node 1 — Upload handler (HTTP controller or flow node)
public async Task<string> HandleUploadAsync(
IFormFile file, CancellationToken ct)
{
using var ms = new MemoryStream();
await file.CopyToAsync(ms, ct);
byte[] bytes = ms.ToArray();
string cid = await _storage.StoreAsync(bytes, file.ContentType, null, ct);
// Store CID as a workflow variable — flows downstream automatically
workflowContext.SetVariable("attachment_cid", cid);
return cid;
}
// Node N — downstream node retrieving the attachment for processing
public async Task ProcessAttachmentAsync(
WorkflowContext ctx, CancellationToken ct)
{
string cid = ctx.GetVariable<string>("attachment_cid");
using var fileStream = await _storage.GetStreamAsync(cid, ct);
// Attach to outbound email, run OCR, virus scan, etc.
await _emailService.SendWithAttachmentAsync(fileStream, ctx.ApproverEmail, ct);
}
Scenario 5 Multimedia & Media Assets
Business Context
Marketing and reporting workflows generate branded PDFs, data exports, chart images, and presentation decks. These assets are often rendered from templates — the same quarterly report layout applied to different periods or regions. When reports share significant template content, CID-based deduplication means identical byte sequences (unchanged sections, logo images, boilerplate pages) are stored once regardless of how many generated documents reference them at the application layer.
Media assets can be large — branded PDFs with embedded charts often exceed 10 MB, and some data export packages reach hundreds of megabytes. InfraHub Storage handles these through standard byte[] upload today, with stream-native support planned.
Implementation
public class ReportPublishingService
{
private readonly IObjectStorageProvider _storage;
public async Task<PublishedReport> PublishAsync(
ReportRenderResult rendered, CancellationToken ct)
{
// Store the rendered PDF — dedup means re-running the same report
// template for the same period returns the same CID instantly
string pdfCid = await _storage.StoreAsync(
rendered.PdfBytes,
"application/pdf",
null,
ct);
// Optionally store a thumbnail image alongside the PDF
string thumbCid = await _storage.StoreAsync(
rendered.ThumbnailPngBytes,
"image/png",
null,
ct);
return new PublishedReport
{
PdfCid = pdfCid,
ThumbnailCid = thumbCid,
PublishedAt = DateTime.UtcNow
};
}
}
MemoryStream before uploading to MinIO. Files over 100 MB should be streamed at the network edge (CDN, reverse proxy) rather than routed through this API to avoid excessive memory pressure on the application container. A streaming StoreStreamAsync overload is planned for a future release.
Scenario 6 Audit Trail Evidence
Business Context
Compliance workflows — SOX, ISO 27001, GDPR data processing audits, financial regulatory filings — require timestamped, tamper-evident evidence records. This might be a screenshot of a system state at a point in time, a digitally signed approval PDF, or an exported log bundle. The key requirement is non-repudiation: evidence stored during an audit period must be provably unchanged when retrieved months later.
InfraHub Storage's immutability property is a perfect fit: because the CID is the SHA-256 hash of the file content, any attempt to silently replace or alter the stored file would produce a different CID — the mismatch is immediately detectable. Storing the CID in a compliance database alongside a timestamp creates an unforgeable link between the event and its evidence file.
Implementation
public class ComplianceEvidenceService
{
private readonly IObjectStorageProvider _storage;
private readonly IComplianceRepository _compliance;
public async Task<EvidenceRecord> StoreEvidenceAsync(
string auditEventID,
byte[] evidenceBytes,
string contentType,
CancellationToken ct)
{
// Store evidence — returns a deterministic, immutable CID
string cid = await _storage.StoreAsync(evidenceBytes, contentType, null, ct);
var record = new EvidenceRecord
{
AuditEventID = auditEventID,
FileCid = cid,
CapturedAt = DateTime.UtcNow,
// ContentHash = cid (the CID IS the SHA-256 hash — no separate hash field needed)
};
// Persist in the compliance database with timestamp
await _compliance.RecordEvidenceAsync(record, ct);
return record;
}
public async Task<bool> VerifyIntegrityAsync(
EvidenceRecord record, CancellationToken ct)
{
// Retrieve the stored file and recompute its SHA-256
using var stream = await _storage.GetStreamAsync(record.FileCid, ct);
byte[] bytes = await stream.ReadAllBytesAsync(ct);
string computedCid = _cidProvider.Compute(bytes);
// If someone replaced the file, computedCid will not match record.FileCid
return computedCid == record.FileCid;
}
}
VerifyIntegrityAsync pattern re-downloads the stored file and recomputes the CID. If the computed CID matches the stored CID, the file is provably unmodified. This integrity check can be run periodically or as part of an audit export to confirm evidence is pristine.
Scenario 7 Data Export & Import Packages
Business Context
InstallHub, data migration pipelines, and tenant onboarding workflows produce ZIP packages containing configuration snapshots, seed data, and schema definitions. These packages are generated on-demand, often large, and have a predictable lifecycle: actively needed during an installation or migration, then dormant and archivable after 30 days, and deletable after a year.
InfraHub Storage's lifecycle archival rules (configured in MinIO — see the Archival & Lifecycle guide) handle this automatically: objects transition to cold storage at Day 30, reducing cost for the long tail of packages that are rarely accessed after the initial installation window.
Implementation
public class InstallPackageService
{
private readonly IObjectStorageProvider _storage;
public async Task<InstallManifest> PublishPackageAsync(
InstallPackage pkg, CancellationToken ct)
{
// Serialise the installation package to a ZIP archive
byte[] zipBytes = await _packager.BuildZipAsync(pkg, ct);
string cid = await _storage.StoreAsync(
zipBytes,
"application/zip",
null,
ct);
return new InstallManifest
{
PackageID = pkg.ID,
ArchiveCid = cid,
GeneratedAt = DateTime.UtcNow,
// Lifecycle policy transitions this to cold storage at Day 30 automatically
ExpiresAt = DateTime.UtcNow.AddDays(365)
};
}
public async Task<Stream> DownloadPackageAsync(
string archiveCid, CancellationToken ct)
{
return await _storage.GetStreamAsync(archiveCid, ct);
}
}
Scenario 8 Multi-Tenant SaaS Isolation
Business Context
In a multi-tenant deployment — where dozens or hundreds of customer organisations share a single BizFirst instance — it is critical that Customer A cannot access Customer B's files, even accidentally. InfraHub Storage enforces this at the storage layer: when PrependTenantID: true, every object key is automatically prefixed with tenant_{tenantID}/ before the CID.
This means isolation is not an application-level concern that can be accidentally bypassed by a bug or missing access check. The storage key itself encodes the tenant boundary. A CID retrieved for tenant 42 (tenant_42/3a7f2e...) is a different object key than the same CID for tenant 99 (tenant_99/3a7f2e...), even if both contain identical content.
How TenantID Is Resolved
Developers never pass TenantID manually. It is resolved automatically from the platform's IAiSessionContextAccessor, which is populated per-request by the BizFirst authentication middleware from the authenticated session token. This ensures:
- No developer can accidentally store or retrieve files for the wrong tenant.
- No API surface exposes TenantID as a parameter — it cannot be spoofed at the application layer.
- Background jobs and agents running in a tenant context inherit the correct TenantID automatically.
Key Prefix Table
| TenantID | Object Key in MinIO | Accessible by Tenant 42? | Accessible by Tenant 99? |
|---|---|---|---|
42 |
tenant_42/3a7f2e8d... |
Yes | No |
99 |
tenant_99/3a7f2e8d... |
No | Yes |
42 |
tenant_42/b81c90f2... |
Yes | No |
Implementation
// Application code — TenantID is never mentioned. The platform handles it.
public class TenantDocumentService
{
private readonly IObjectStorageProvider _storage;
// IAiSessionContextAccessor is injected internally by IObjectStorageProvider
// Application services do NOT need to inject or handle it.
public async Task<string> StoreForCurrentTenantAsync(
byte[] data, string contentType, CancellationToken ct)
{
// PrependTenantID=true means the stored key will be:
// tenant_{currentTenantID}/{sha256_of_data}
// No explicit TenantID parameter — isolation is automatic.
return await _storage.StoreAsync(data, contentType, null, ct);
}
public async Task<Stream> GetForCurrentTenantAsync(
string cid, CancellationToken ct)
{
// Only retrieves objects under the current tenant's prefix.
// A CID belonging to a different tenant will return null / not found.
return await _storage.GetStreamAsync(cid, ct);
}
}
IObjectStorageProvider. Even if application code has a bug that passes a CID from the wrong tenant, the key prefix mismatch will cause a not-found result — not a cross-tenant data leak.
Storage Cost Estimation
The table below provides rough size and volume estimates for common scenarios. Use these as a planning baseline when sizing your MinIO disk allocation or estimating cloud storage spend. Actual figures depend on document complexity, image resolution, and compression ratios.
| Scenario | Typical File Size | Monthly Volume | Estimated Storage / Month |
|---|---|---|---|
| Invoice PDFs | 150 KB | 10,000 files | ~1.5 GB |
| HR Documents | 500 KB | 500 files | ~250 MB |
| AI Artefacts | 2 MB | 200 files | ~400 MB |
| Workflow Attachments | 1 MB | 2,000 files | ~2 GB |
| Multimedia / Reports | 5 MB | 400 files | ~2 GB |
| Audit Evidence | 200 KB | 1,000 files | ~200 MB |
| Data Export Packages | 20 MB | 50 packages | ~1 GB |
Cloud Provider Monthly Pricing Reference (approximate)
| Backend | Storage Price | Egress Price | Notes |
|---|---|---|---|
| MinIO (self-hosted) | Cost of disk hardware / cloud VM disk | Internal network only | Lowest total cost for high-volume on-prem workloads |
| AWS S3 (Standard) | ~$0.023 / GB | ~$0.09 / GB | Switch to S3-IA for infrequently accessed archives |
| Cloudflare R2 | ~$0.015 / GB | $0.00 (free egress) | Best cost profile for download-heavy scenarios |
| Backblaze B2 | ~$0.006 / GB | ~$0.01 / GB | Lowest storage cost for backup and archival targets |