Media Service
Enterprise media platform for SaaS products. Direct-to-cloud uploads, secure delivery, async processing pipelines, malware scanning, governance, and billing metrics — all from one API.
Overview
The Media Service is a standalone media platform for SaaS products. It manages the entire file lifecycle — from the first presigned upload URL issued to the last compliance record retained. Frontend and backend applications should treat it as the single source of truth for all product files and media assets.
The API is split into six logical service surfaces, each accessible via ConnectRPC (REST-JSON and gRPC from the same server):
Core Concepts
Understanding the key domain objects will help you model file workflows correctly in your product.
FileAssetThe primary object your application works with. Represents the logical file in the product — metadata, current status, permissions, visibility, and the active version.
idtitledescriptionoriginal_file_namemime_typemedia_kindvisibilitylifecycle_statusapproval_statusmalware_scan_statustagscustom_metadatacreated_atdeleted_athas_previewhas_transcripthas_extracted_textFileVersionA FileAsset can have multiple FileVersion records. Use for document history, replacing an asset without changing its ID, rollback, and auditability.
idfile_asset_idversion_numberstored_object_idsize_byteschecksum_sha256created_atis_activeUploadSessionDefines how the client uploads bytes: direct upload URL, multipart upload parts, resumable settings, and expiration. Returned by InitiateUpload.
idupload_urlrequired_headersupload_methodpart_upload_urlsexpires_atRenditionA derived output produced by a processing job: thumbnail, preview image, transcoded video, audio transcode, watermarked version, or zip archive.
idfile_asset_idrendition_typemime_typesize_byteswidthheightduration_msstatusShareLinkA public or semi-public link for private content. Supports password, expiry, max downloads, IP restrictions, and authenticated-user requirements.
idfile_asset_idurlexpires_atpassword_protectedmax_downloadsdownload_countis_revokedProcessingJobAn async operation: OCR, transcription, video processing, moderation, thumbnail generation, or image transforms. Poll status until a terminal state is reached.
idfile_asset_idjob_typestatuscreated_atcompleted_aterror_messageLifecycle States
Every FileAsset moves through a deterministic state machine. Your UI must handle every state explicitly — never assume a file is usable immediately after upload completes.
PENDING_UPLOAD→UPLOADING→PROCESSING→READY / QUARANTINED / FAILED→SOFT_DELETED→ARCHIVED| Status | Meaning | UI Action |
|---|---|---|
PENDING_UPLOAD | Upload session created, no bytes received yet | Show upload prompt or progress bar |
UPLOADING | Client is actively transferring bytes | Show transfer progress percentage |
PROCESSING | Bytes received, async jobs running | Show 'Processing...' state, disable preview |
READY | All required jobs succeeded, file is fully usable | Show file card, enable all actions |
QUARANTINED | Malware detected — file is blocked | Show safe error state, block download |
FAILED | A required processing step failed permanently | Show error badge, offer re-upload |
SOFT_DELETED | File is in trash, recoverable within retention window | Show in trash bin view with restore option |
ARCHIVED | File moved to cold storage, not immediately accessible | Show archived badge, offer restore on demand |
Upload Flow
The canonical upload flow is always three steps: Initiate → Direct Upload → Complete. The service never receives file bytes — it only orchestrates.
PROCESSING and becomes usable only after all async jobs succeed.Direct-to-Cloud Upload
The most important architectural rule: file bytes never travel through your application backend. The service issues a presigned URL and the client uploads directly to S3 or GCS. This eliminates backend OOM crashes, timeout failures, and egress costs on large uploads.
// Step 1: Initiate — get presigned upload URL
const initResp = await mediaClient.initiateUpload({
tenantId: tenantID,
ownerId: userID,
originalFileName: file.name,
fileSizeBytes: BigInt(file.size),
mimeType: file.type,
title: "Service Agreement v2",
tags: ["contract", "legal"],
entityRef: { entityType: "ticket", entityId: ticketID },
});
const { file: asset, version, upload } = initResp;
// asset.lifecycleStatus === "PENDING_UPLOAD"
// Step 2: Upload bytes directly to cloud storage (not your backend)
await fetch(upload.uploadUrl, {
method: "PUT",
body: file,
headers: Object.fromEntries(
upload.requiredHeaders.map(h => [h.name, h.value])
),
});
// Step 3: Notify the service that upload is done
await mediaClient.completeUpload({
uploadSessionId: upload.id,
fileVersionId: version.id,
});
// Step 4: Poll until READY (or FAILED / QUARANTINED)
const poll = setInterval(async () => {
const { file } = await mediaClient.getFileAsset({ fileAssetId: asset.id });
if (file.lifecycleStatus === "READY") {
clearInterval(poll);
setFileReady(file);
} else if (["FAILED", "QUARANTINED"].includes(file.lifecycleStatus)) {
clearInterval(poll);
setFileError(file.lifecycleStatus);
}
}, 2000);part_upload_urls for multipart upload. Complete each part and pass the ETags to CompleteUpload.AbortUpload and restart with a fresh InitiateUpload call.Download & Playback
Never construct raw file URLs manually. All download and playback URLs are short-lived, server-generated, and issued only after permission checks pass. Hardcoding bucket paths bypasses access control entirely.
Use for downloading original files, specific versions, or derived renditions (thumbnails, previews).
- › Returns
url, optionalheaders, andexpires_at - › Set
disposition: ATTACHMENTfor download,INLINEfor preview - › Request a specific
rendition_idfor thumbnails
Use for streaming video and audio assets. Returns a session with playback URL, supported formats, and available caption tracks.
- › Returns
playback_url,caption_tracks,renditions - › Session is time-limited — generate just before the player renders
- › Supports HLS and DASH adaptive streaming
resp, err := client.GenerateDownloadLink(ctx, connect.NewRequest(
&mediav1.GenerateDownloadLinkRequest{
FileAssetId: fileID,
TenantId: tenantID,
Disposition: mediav1.Disposition_INLINE, // or ATTACHMENT
// Optional: request a specific rendition (e.g., thumbnail)
RenditionId: thumbnailRenditionID,
},
))
if err != nil {
return err
}
// Use resp.Url for the browser fetch / src attribute
// Never cache beyond resp.ExpiresAt
log.Printf("URL valid until: %s", resp.Msg.ExpiresAt.AsTime())localStorage, cache them in global state, or embed them in href attributes that persist across renders. Request a fresh URL per action.Processing & Jobs
Many media features are asynchronous. After CompleteUpload, the service enqueues jobs based on MIME type. Frontend must never assume derived outputs are immediately available.
has_extracted_texthas_transcriptrenditions listrenditions listhas_previewmoderation_statusSUCCEEDEDJob completed successfully. Output artifacts are available.FAILEDJob failed permanently. Error message is present on the job object.CANCELEDJob was explicitly canceled before completion.// Manually trigger a specific processing job
resp, err := client.CreateProcessingJob(ctx, connect.NewRequest(
&mediav1.CreateProcessingJobRequest{
FileAssetId: fileID,
TenantId: tenantID,
JobType: mediav1.ProcessingJobType_OCR,
// Optional: override default parameters
OcrParams: &mediav1.OcrParams{
Language: "en",
OutputFormat: mediav1.OcrOutputFormat_SEARCHABLE_PDF,
},
},
))
jobID := resp.Msg.Job.Id
// Poll job status
for {
status, _ := client.GetProcessingJob(ctx, connect.NewRequest(
&mediav1.GetProcessingJobRequest{JobId: jobID},
))
if status.Msg.Job.Status == mediav1.JobStatus_SUCCEEDED {
// Retrieve OCR output
text, _ := client.GetExtractedText(ctx, connect.NewRequest(
&mediav1.GetExtractedTextRequest{FileAssetId: fileID},
))
break
}
time.Sleep(2 * time.Second)
}Search & Catalog
The catalog listing supports rich server-side filtering. Never load the full file list client-side — use the page_token cursor for pagination.
folder_idFilter to a specific folderrecursiveInclude files in nested subfoldersqueryFull-text search on title, filename, tagsocr_querySearch inside OCR-extracted document texttagsMatch one or more tag valuesmedia_kindIMAGE, VIDEO, AUDIO, DOCUMENT, ARCHIVE, OTHERlifecycle_statusREADY, PROCESSING, QUARANTINED, SOFT_DELETEDcreated_after / created_beforeDate range filtermin_size_bytes / max_size_bytesFile size range filterpage_token / page_sizeCursor paginationpage_token as opaque. Preserve current filters in your URL / route state so they survive navigation and browser back. Support lifecycle_status filter chips in the UI: Ready, Processing, Quarantined, Deleted.Governance
The Governance surface covers compliance, billing, and operational visibility. It is typically only accessible to admin-level product roles.
Configure automatic lifecycle transitions — soft delete after N days of inactivity, purge from cold storage after N days in trash, archive to cold tier after N days. Applied per tenant, folder, or MIME kind.
Place files under legal hold to prevent deletion or modification for a specified period. Suitable for financial records, healthcare documents, and regulated content. Even admins cannot override an active legal hold.
Every upload is scanned before the file enters READY state. A detected threat sets lifecycle_status to QUARANTINED — downloads and previews are blocked automatically. Results are available via GetFileAsset.
Set storage, file count, bandwidth, and processing limits per tenant or user. GetUsageReport returns storage consumption, egress bandwidth, processing minutes, and request volume — all billable dimensions.
Every file access, upload, download, deletion, and share-link creation is recorded in an immutable audit log. ListAuditEvents supports filtering by actor, resource, action type, and date range.
Register HTTP webhook endpoints to receive file lifecycle events: media.uploaded, media.ready, media.quarantined, media.virus_scanned, media.video_processed, media.share_link_created, media.share_link_revoked. Retry delivery is automatic.
Feature Matrix by Media Type
Different media types trigger different processing pipelines automatically after upload completes.
| Feature | ||||
|---|---|---|---|---|
| Image | Video | Audio | Document | |
| Direct upload | ||||
| Multipart / resumable upload | — | |||
| Thumbnails | — | |||
| Preview generation | — | — | ||
| Video transcoding (HLS/DASH) | — | — | — | |
| Streaming playback session | — | — | ||
| Transcription / subtitles | — | — | ||
| OCR / extracted text | — | — | — | |
| AI auto-tagging | — | — | ||
| Moderation | — | — | ||
| Dynamic watermarking | — | — | ||
| Annotations | — | |||
| Version history | ||||
| Secure download link | ||||
| Governed share links |
Security Rules
The service enforces security server-side, but the frontend must behave safely to avoid bypassing those controls.
Do
- › Always request server-generated download / playback / share URLs
- › Use the authenticated session for all secure operations
- › Show QUARANTINED state clearly — block preview and download
- › Respect backend access failures (403, 404)
- › Request a fresh download URL per user action
- › Check
has_preview,has_transcriptbefore rendering derived content
Do Not
- › Hardcode storage bucket URLs or object keys
- › Expose internal object keys in UI or href attributes
- › Keep presigned URLs in localStorage or long-lived state
- › Upload file bytes to your own backend server
- › Assume public visibility means unlimited access forever
- › Bypass access-check flows or construct manual CDN URLs
Integration Checklist
Verify these before shipping any media integration.