GCS

Lightweight, dependency-free fake of Google Cloud Storage that speaks the real GCS JSON API (https://storage.googleapis.com/storage/v1), so application code using @google-cloud/storage can run against it with zero cost and zero side effects.

KeyValue
Port4580
ProtocolGCS JSON API (HTTP + JSON)
Compatible client@google-cloud/storage (v7)
Size~80 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Quick Start

Start the server:

import { GcsServer } from "./services/gcs/src/server.js";

const server = new GcsServer(4580);
await server.start();
// ... use it ...
await server.stop();

Connect with the real Google Cloud Storage client. Point it at the fake with either apiEndpoint or the STORAGE_EMULATOR_HOST environment variable:

import { Storage } from "@google-cloud/storage";

const storage = new Storage({
  apiEndpoint: "http://127.0.0.1:4580",
  projectId: "parlel",
  // Any credentials work — the fake does not verify them.
  credentials: {
    client_email: "parlel@parlel.iam.gserviceaccount.com",
    private_key: "fake",
  },
});

const [bucket] = await storage.createBucket("my-bucket");

const file = bucket.file("hello.txt");
await file.save("hello world", { contentType: "text/plain" });

const [contents] = await file.download();
console.log(contents.toString()); // "hello world"

Or via the emulator environment variable (no apiEndpoint needed):

export STORAGE_EMULATOR_HOST=http://127.0.0.1:4580

Authentication

Google credentials and OAuth tokens are accepted but not verified (any credentials work). This matches LocalStack-style local development. Client-side operations such as getSignedUrl() still require a syntactically valid RSA private key because the signing happens in the client.

Resettable state

All state is in-memory and ephemeral. Reset it between tests:

// Programmatically
server.reset();

// Or over HTTP
await fetch("http://127.0.0.1:4580/_parlel/reset", { method: "POST" });

Implemented operations / endpoints

Service / internal

OperationEndpoint
Health checkGET /_parlel/health
Reset statePOST /_parlel/reset
Get service accountGET /storage/v1/projects/{project}/serviceAccount

Buckets

OperationEndpoint
Insert (create) bucketPOST /storage/v1/b
List bucketsGET /storage/v1/b
Get bucketGET /storage/v1/b/{bucket}
Patch bucketPATCH /storage/v1/b/{bucket}
Update bucketPUT /storage/v1/b/{bucket}
Delete bucketDELETE /storage/v1/b/{bucket}

Bucket convenience methods that route through patch/update are supported: setMetadata, setLabels, getLabels, deleteLabels, setStorageClass, setCorsConfiguration, addLifecycleRule, enableRequesterPays, setRetentionPeriod, and toggling versioning.

Objects

OperationEndpoint
List objects (prefix, delimiter, versions, pagination)GET /storage/v1/b/{bucket}/o
Get object metadataGET /storage/v1/b/{bucket}/o/{object}
Download object mediaGET /storage/v1/b/{bucket}/o/{object}?alt=media
Patch object metadataPATCH /storage/v1/b/{bucket}/o/{object}
Update object metadataPUT /storage/v1/b/{bucket}/o/{object}
Delete objectDELETE /storage/v1/b/{bucket}/o/{object}
Copy objectPOST /storage/v1/b/{src}/o/{srcObj}/copyTo/b/{dst}/o/{dstObj}
Rewrite objectPOST /storage/v1/b/{src}/o/{srcObj}/rewriteTo/b/{dst}/o/{dstObj}
Compose objectsPOST /storage/v1/b/{bucket}/o/{object}/compose
Public / XML-style read (for isPublic/publicUrl)GET /{bucket}/{object}

Object convenience methods that work on top of these: save, download (to memory or disk), createReadStream, createWriteStream, exists, getMetadata, setMetadata, copy, move, rename, makePublic, makePrivate, isPublic, setStorageClass, and ranged downloads ({ start, end }).

Uploads

Upload typeEndpoint
Simple media uploadPOST /upload/storage/v1/b/{bucket}/o?uploadType=media&name=...
Multipart upload (JSON metadata + media)POST /upload/storage/v1/b/{bucket}/o?uploadType=multipart
Resumable: start sessionPOST /upload/storage/v1/b/{bucket}/o?uploadType=resumable
Resumable: upload chunk(s)PUT /upload/storage/v1/b/{bucket}/o?upload_id=...

Both single-shot resumable uploads (Content-Range: bytes 0-*/*) and multi-chunk resumable uploads (with chunkSize, returning 308 between chunks with a Range header) are supported.

ACLs (canned)

OperationEndpoint
Object ACL list/get/insert/update/delete.../o/{object}/acl[/{entity}]
Bucket ACL list/get/insert/update/delete/storage/v1/b/{bucket}/acl[/{entity}]
Default object ACL/storage/v1/b/{bucket}/defaultObjectAcl[/{entity}]

IAM (canned)

OperationEndpoint
Get IAM policyGET /storage/v1/b/{bucket}/iam
Set IAM policyPUT /storage/v1/b/{bucket}/iam
Test IAM permissionsGET /storage/v1/b/{bucket}/iam/testPermissions

HMAC keys

OperationEndpoint
Create HMAC keyPOST /storage/v1/projects/{project}/hmacKeys
List HMAC keysGET /storage/v1/projects/{project}/hmacKeys
Get HMAC keyGET /storage/v1/projects/{project}/hmacKeys/{accessId}
Update HMAC keyPUT /storage/v1/projects/{project}/hmacKeys/{accessId}
Delete HMAC keyDELETE /storage/v1/projects/{project}/hmacKeys/{accessId}

Notifications (canned)

OperationEndpoint
List / create / get / delete notification configs/storage/v1/b/{bucket}/notificationConfigs[/{id}]

Feature support

FeatureSupportedNotes
Bucket CRUDCreate, get, list, patch, delete
Object upload (simple / multipart / resumable)Including chunked resumable uploads
Object downloadFull + ranged (Range / { start, end })
crc32c + md5 hashesReal CRC32C (Castagnoli); client-side validation passes
Object metadata (custom + system)metadata, contentType, cacheControl, etc.
Listing (prefix, delimiter, prefixes)Plus name-indexed pagination via pageToken
PaginationmaxResults + pageToken (nextPageToken in response)
Object versioning / generationsPer-bucket versioning.enabled; multiple generations retained
PreconditionsifGenerationMatch/NotMatch, ifMetagenerationMatch/NotMatch
Copy / move / rename / rewrite / compose
ACLs (object / bucket / default)⚠️Accepted and echoed; not enforced
IAM policy + testPermissions⚠️Canned responses; not enforced
HMAC keysFull lifecycle, in-memory
Signed URLs (getSignedUrl)Generated client-side; needs a valid RSA key
Notifications (Pub/Sub)⚠️Endpoints respond; no events are delivered
Batch requests⚠️Acknowledged with an empty multipart batch response
KMS / CMEK encryption✓ By design — Plain in-memory storage — transport/at-rest crypto is unnecessary locally
Retention / object hold enforcement⟳ Roadmap
Requester Pays billing⟳ Roadmap
Real Google auth / quota✓ By design — Never throttles — local tests run at full speed, zero cost

Legend: ✅ fully supported · ✓ by design (intentional for a local emulator) · ⟳ on the roadmap.

Error codes & shapes

Errors are returned as the standard GCS JSON error envelope:

{
  "error": {
    "code": 404,
    "message": "No such object: my-bucket/missing.txt",
    "errors": [
      {
        "domain": "global",
        "reason": "notFound",
        "message": "No such object: my-bucket/missing.txt"
      }
    ]
  }
}
StatusReasonWhen
400invalid / requiredInvalid bucket name, missing name, malformed JSON/multipart
404notFoundMissing bucket, object, upload session, or HMAC key
409conflictBucket already exists, or deleting a non-empty bucket
412conditionNotMetFailed ifGenerationMatch / ifMetagenerationMatch precondition
304conditionNotMetifGenerationNotMatch / ifMetagenerationNotMatch matched
405methodNotAllowedUnsupported HTTP method for a route
500internalErrorUnexpected server error

Successful downloads include x-goog-hash (crc32c=...,md5=...), x-goog-generation, x-goog-metageneration, and x-goog-stored-content-length headers, mirroring the real service.

Environment variables

VariableDefaultPurpose
STORAGE_EMULATOR_HOSThttp://127.0.0.1:4580Points @google-cloud/storage at the fake
GOOGLE_CLOUD_PROJECTparlelDefault project id
GCLOUD_PROJECTparlelDefault project id (legacy var)
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

STORAGE_EMULATOR_HOST=http://parlel-bridge:4580
GOOGLE_CLOUD_PROJECT=parlel
GCLOUD_PROJECT=parlel
<!-- parlel:testenv:end -->