Launch Rail Logo
Launch Rail
Documentation

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

Catalog
Browse, query, update, move, copy, soft-delete, restore, and version managed assets.
Folders
Create, list, rename, and delete logical folder groupings for file organization.
Ingest
Initiate uploads, resume transfers, complete uploads, abort sessions, and import from remote sources.
Access
Generate secure download links, playback sessions, share links, and purge delivery caches.
Processing
Create processing jobs, poll status, list renditions, get transcripts, OCR text, and manage annotations.
Governance
Manage retention, legal hold, quotas, usage reporting, audit events, bulk downloads, and webhooks.

Core Concepts

Understanding the key domain objects will help you model file workflows correctly in your product.

FileAsset

The 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_text
FileVersion

A 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_active
UploadSession

Defines how the client uploads bytes: direct upload URL, multipart upload parts, resumable settings, and expiration. Returned by InitiateUpload.

idupload_urlrequired_headersupload_methodpart_upload_urlsexpires_at
Rendition

A 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_msstatus
ShareLink

A 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_revoked
ProcessingJob

An 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_message

Lifecycle 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
StatusMeaningUI Action
PENDING_UPLOADUpload session created, no bytes received yetShow upload prompt or progress bar
UPLOADINGClient is actively transferring bytesShow transfer progress percentage
PROCESSINGBytes received, async jobs runningShow 'Processing...' state, disable preview
READYAll required jobs succeeded, file is fully usableShow file card, enable all actions
QUARANTINEDMalware detected — file is blockedShow safe error state, block download
FAILEDA required processing step failed permanentlyShow error badge, offer re-upload
SOFT_DELETEDFile is in trash, recoverable within retention windowShow in trash bin view with restore option
ARCHIVEDFile moved to cold storage, not immediately accessibleShow 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.

1
User selects a file
Read name, size, and MIME type from the file input.
2
Call InitiateUpload
POST /v1/media/ingest/initiate — returns file, version, and upload session with the presigned URL.
3
Upload bytes directly to cloud
PUT the file to upload.upload_url with any upload.required_headers. This goes to S3/GCS — not your backend.
4
Call CompleteUpload
POST /v1/media/ingest/complete — triggers malware scan, processing pipeline, and status transitions.
5
Poll file status
GET /v1/media/catalog/{id} until lifecycle_status reaches READY, FAILED, or QUARANTINED.
Never assume file readiness immediately after CompleteUpload. The file enters 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);
For large files (>100 MB), the upload session will provide part_upload_urls for multipart upload. Complete each part and pass the ETags to CompleteUpload.
If a session expires before upload completes, call 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.

GenerateDownloadLink

Use for downloading original files, specific versions, or derived renditions (thumbnails, previews).

  • › Returns url, optional headers, and expires_at
  • › Set disposition: ATTACHMENT for download, INLINE for preview
  • › Request a specific rendition_id for thumbnails
GeneratePlaybackSession

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())
Download URLs are short-lived. Never store them in 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.

OCR text extraction
Check: has_extracted_text
Transcription (VTT/SRT)
Check: has_transcript
Video renditions / HLS
Check: renditions list
Thumbnail generation
Check: renditions list
Document preview pages
Check: has_preview
Moderation result
Check: moderation_status
Processing job terminal states
SUCCEEDEDJob 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.

ListFileAssets — supported filters
folder_idFilter to a specific folder
recursiveInclude files in nested subfolders
queryFull-text search on title, filename, tags
ocr_querySearch inside OCR-extracted document text
tagsMatch one or more tag values
media_kindIMAGE, VIDEO, AUDIO, DOCUMENT, ARCHIVE, OTHER
lifecycle_statusREADY, PROCESSING, QUARANTINED, SOFT_DELETED
created_after / created_beforeDate range filter
min_size_bytes / max_size_bytesFile size range filter
page_token / page_sizeCursor pagination
Treat page_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.

Retention Policies

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.

Legal Hold & WORM Immutability

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.

Malware Scanning

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.

Quotas & Usage Reporting

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.

Audit Events

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.

Webhooks

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
ImageVideoAudioDocument
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_transcript before 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.

Uploads use the direct-to-cloud flow (no backend file proxy)
File status handling covers PROCESSING, READY, FAILED, QUARANTINED
Previews handle missing derived assets gracefully (has_preview check)
Share links display expiry and revoke action
Download / playback URLs are requested just-in-time
Pagination uses page_token (never client-side full load)
Destructive actions (delete, revoke) require user confirmation
UI respects soft-delete / restore behavior
Admin-only governance screens are product-level gated
QUARANTINED files are clearly blocked with a safe error state