Pushduck
Pushduck// S3 uploads for any framework

Google Cloud Storage

Upload to Google Cloud Storage through its S3-compatible XML API. Step-by-step setup in the Console or with gcloud — project, bucket, HMAC key, CORS and a working config.

Using Google Cloud Storage

Google Cloud Storage serves an S3-compatible XML API at https://storage.googleapis.com. Pushduck uploads through it with an HMAC key — an access ID starting GOOG and a secret — so presigned uploads and downloads, multipart uploads and the Storage API work the same way they do on S3.

Service-account JSON key files do not work here. You need an HMAC key, created under Cloud Storage → Settings → Interoperability. Google's Simple migration from Amazon S3 describes the same setup.

Everything on this page was run against a real bucket: presigned PUT and GET, a three-part multipart upload, list, metadata and delete, and the router's presign → upload → complete flow behind a CORS preflight.

What You Will End Up With

You getLooks likeGoes into
Bucket namemy-app-uploadsGCS_BUCKET
HMAC Access keyGOOG1E…GCS_ACCESS_KEY_ID
HMAC Secret40 characters, shown onceGCS_SECRET_ACCESS_KEY

Before You Start

  • A Google Cloud project with billing enabled. Cloud Storage refuses to create buckets in a project without a billing account, even on the free tier. Check it at Billing → Linked account.

  • The Cloud Storage API enabled — it usually is. Check at APIs & Services → Cloud Storage API.

  • For the command-line steps only: the gcloud CLI, signed in and pointed at your project:

    gcloud auth login
    gcloud config set project YOUR_PROJECT_ID

Create a Bucket

  1. Open Cloud Storage → Buckets → Create.
  2. Name: a globally unique name, e.g. my-app-uploads.
  3. Location: a region near your users, e.g. us-central1.
  4. Storage class: Standard.
  5. Access control: leave Uniform selected, and leave Enforce public access prevention on unless the files must be publicly readable (see Public files).
  6. Click Create.
gcloud storage buckets create gs://my-app-uploads \
  --location=us-central1 \
  --uniform-bucket-level-access

Uniform access means permissions come from IAM and per-object ACLs are rejected. Pushduck never sends ACLs to Google Cloud Storage, so this is the right setting.

Create an HMAC Key

An HMAC key belongs either to a service account (recommended — it can be limited to this one bucket) or to your own user account (quickest, but it carries all of your Cloud Storage access).

  1. Create the service account. IAM & Admin → Service Accounts → Create service account. Name it pushduck-uploads, click Create and continue, skip the role step, and click Done.
  2. Give it access to the bucket only. Open your bucket in Cloud Storage → Buckets → Permissions → Grant access. Principal: pushduck-uploads@YOUR_PROJECT_ID.iam.gserviceaccount.com. Role: Storage Object Admin. Click Save.
  3. Create its HMAC key. Cloud Storage → Settings → Interoperability → Service account HMAC → Create a key for a service account → choose pushduck-uploads → Create key.
  4. Copy the Access key and the Secret now. The secret is never shown again.
gcloud iam service-accounts create pushduck-uploads

gcloud storage buckets add-iam-policy-binding gs://my-app-uploads \
  --member="serviceAccount:pushduck-uploads@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/storage.objectAdmin"

gcloud storage hmac create \
  pushduck-uploads@YOUR_PROJECT_ID.iam.gserviceaccount.com

The output contains accessId (the access key) and secret. The secret is never shown again.

  1. Open Cloud Storage → Settings → Interoperability.
  2. Scroll to Access keys for your user account and click Create a key.
  3. Copy the Access key and the Secret.

This key can do anything your account can do in Cloud Storage, in every project you can access. Use it for development, or when your organization blocks service-account keys.

HTTPError 412: Request violates constraint 'constraints/iam.disableServiceAccountKeyCreation' — many organizations enforce this policy, which blocks service-account HMAC keys. Either ask an Organization Policy Administrator to allow key creation on this project, or use a user-account key.

Allow Your Site to Upload (CORS)

The browser uploads straight to Google Cloud Storage, so the bucket must allow your site's origin. Multipart uploads read each part's ETag, so it must be exposed. The Console has no CORS editor; use gcloud (Cloud Shell works — click the terminal icon at the top of the Console).

cors.json
[
  {
    "origin": ["http://localhost:3000", "https://yourdomain.com"],
    "method": ["GET", "PUT", "HEAD"],
    "responseHeader": ["Content-Type", "ETag"],
    "maxAgeSeconds": 3600
  }
]
gcloud storage buckets update gs://my-app-uploads --cors-file=cors.json

This is Google Cloud Storage's own CORS format — not the S3 CORSRules format other providers use.

Configure Your App

.env.local
GCS_BUCKET=my-app-uploads
GCS_ACCESS_KEY_ID=GOOG1E...
GCS_SECRET_ACCESS_KEY=...
lib/upload.ts
import { createUploadConfig } from "pushduck/server";

export const { s3, storage } = createUploadConfig()
  .provider("gcs", {
    bucket: process.env.GCS_BUCKET!,
    accessKeyId: process.env.GCS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.GCS_SECRET_ACCESS_KEY!,
  })
  .build();

That is the whole configuration. The defaults below are what was tested; change them only for a reason.

OptionRequiredDefaultDescription
bucketYesThe bucket name.
accessKeyIdYesHMAC access key (GOOG…).
secretAccessKeyYesHMAC secret.
regionNoautoSigning region. auto works for every location; the bucket's own location also works.
endpointNohttps://storage.googleapis.comThe XML API endpoint.
forcePathStyleNotruefalse addresses the bucket as my-app-uploads.storage.googleapis.com.
customDomainNoA CDN or load balancer in front of a public bucket, used for public URLs.

acl is accepted and ignored: Google Cloud Storage controls access with IAM.

Never expose these variables to the browser — do not prefix them with NEXT_PUBLIC_, VITE_ or PUBLIC_.

Test Your Setup

From your server code (an API route or a script), list the bucket:

import { storage } from "@/lib/upload";

const files = await storage.list.files({ maxFiles: 1 });
console.log("Connected to Google Cloud Storage —", files.length, "object(s) listed");

If that prints, signing, the key and bucket access all work. Then upload a file from your app to confirm CORS.

Reading Uploaded Files

Private files

Objects are private by default. Hand out short-lived signed URLs:

const url = await storage.download.presignedUrl("avatars/me.png", 3600);

Public files

To serve files without signed URLs:

  1. In the bucket's Permissions tab, turn off Public access prevention.
  2. Grant access → principal allUsers → role Storage Object Viewer.

Objects are then readable at https://storage.googleapis.com/my-app-uploads/<key>, or through a CDN set as customDomain.

Rotating or Deleting a Key

HMAC keys do not expire. To rotate one, create a new key, deploy it, then on Settings → Interoperability set the old key to Inactive and delete it. For a service-account key:

gcloud storage hmac update ACCESS_ID --deactivate
gcloud storage hmac delete ACCESS_ID

Common Issues

Google Cloud Storage needs an HMAC key … not a service-account key → The config passes projectId or keyFilename. Replace them with accessKeyId and secretAccessKey from an HMAC key.

Bucket creation fails, or "billing account" errors → The project has no billing account. Link one under Billing.

HTTPError 412: Request violates constraint 'constraints/iam.disableServiceAccountKeyCreation' → Your organization blocks service-account keys. See Create an HMAC Key.

403 SignatureDoesNotMatch → The secret is wrong, or the key was deactivated.

403 AccessDenied → The key's account has no role on the bucket. Grant Storage Object Admin on the bucket.

400 Cannot insert legacy ACL for an object when uniform bucket-level access is enabled → You are on the generic s3Compatible provider, which sends an ACL. Use .provider("gcs", …).

Browser request blocked by CORS → Check the bucket's CORS rules include your origin and PUT, and expose ETag for multipart uploads.


Next: Upload Your First Image or see S3-Compatible for other services.