You may not need this package
GCS speaks the S3 XML API in interoperability mode, so the S3 provider reaches it today:
createS3StorageProvider({
bucket,
endpoint: 'https://storage.googleapis.com',
credentials: { accessKeyId: HMAC_ACCESS_ID, secretAccessKey: HMAC_SECRET }
});That is one fewer package to keep, and it is the right answer for most deployments.
This adapter exists for the one it is not. HMAC keys are a long-lived secret many organizations forbid by policy, and they rule out Workload Identity. Native authentication is the reason to be here.
Install
npm install @apograph/media-provider-gcsimport { createGcsStorageProvider } from '@apograph/media-provider-gcs';
MediaServerPlugin({
provider: createGcsStorageProvider(config.plugins.media.storage),
config: config.plugins.media
});Configuration
{ bucket, projectId?, keyFilename?, credentials?, keyPrefix?, signWithIam?, bucketClient? }
| Variable | What it does |
|---|---|
MEDIA_GCS_BUCKET | The bucket name. Required |
MEDIA_GCS_PROJECT_ID | Optional; Application Default Credentials otherwise |
MEDIA_GCS_KEY_FILE | Path to a service-account key file |
MEDIA_GCS_SIGN_WITH_IAM | Sign through IAM rather than a private key |
With no key file and no inline credentials the client uses Application Default Credentials, which is what a GKE or Cloud Run deployment wants.
bucketClient takes an already-built Bucket — hand-built auth, or a test
stub.
directUrl is computed, not hardcoded
Signing needs a private key, or an explicit opt-in to IAM signBlob.
| How you authenticate | directUrl |
|---|---|
keyFilename or inline credentials | true |
signWithIam: true | true |
| Bare Application Default Credentials | false — downloads are proxied |
signWithIam is opt-in, not assumed
It needs the iam.serviceAccounts.signBlob permission. Assuming it would mint
URLs that fail at request time, and would let the plugin accept
MEDIA_DIRECT_SERVE=signed-url on a deployment that cannot honour it.
Under Workload Identity, this is how you sign: the library asks IAM to sign for it.
What not to lose in a refactor
get reads metadata before it opens the stream. createReadStream opens
lazily, so a missing object would surface as an error on the stream — by which
point the response is already a streaming 200 that can no longer become the 404
the route owes the caller.
One extra call buys that back. It is the port’s contract, not an optimization waiting to be removed.
A failed upload deletes the object. A resumable upload that dies part-way leaves an incomplete object the bucket keeps until a lifecycle rule sweeps it, and the key never reached a caller.