typescript 118 lines · 3 tabs

GraphQL persisted queries (hash allowlist)

Shared by codesnips Jan 2026
3 tabs
import { createHash } from "crypto";
import { readFileSync } from "fs";

export function sha256(query: string): string {
  return createHash("sha256").update(query, "utf8").digest("hex");
}

type RawManifest = Record<string, string>;

export function loadManifest(path: string): ReadonlyMap<string, string> {
  const raw = JSON.parse(readFileSync(path, "utf8")) as RawManifest;
  const map = new Map<string, string>();

  for (const [hash, query] of Object.entries(raw)) {
    const actual = sha256(query);
    if (actual !== hash) {
      throw new Error(
        `Manifest integrity error: entry ${hash} does not match its query (got ${actual})`
      );
    }
    map.set(hash, query);
  }

  return Object.freeze(map);
}
3 files · typescript Explain with highlit

Persisted queries flip the usual GraphQL contract: instead of clients sending arbitrary query strings, they send only a SHA-256 hash that maps to a query the server already knows. This snippet implements the stricter allowlist variant, where any hash not present in a build-time manifest is rejected outright. That closes off two problems at once — it shrinks request payloads to a tiny hash, and it prevents attackers from crafting expensive or introspection-heavy queries, since only vetted operations can run.

The query-manifest file loads a JSON map produced during the client build (typically by a codegen tool that walks all .graphql documents). loadManifest reads that file once, verifies each entry's hash actually matches its query text via sha256, and returns a frozen Map. Verifying at load time catches a corrupted or tampered manifest early rather than mid-request. The sha256 helper is the single source of truth for how hashes are computed, so client and server stay in agreement.

The heart of the enforcement lives in persistedQueryPlugin, an Apollo Server plugin implementing the requestDidStart lifecycle. In didResolveSource it reads the APQ extension (extensions.persistedQuery.sha256Hash) that Apollo's link protocol sends. If a hash is present, it is looked up in the manifest and the corresponding query is injected into the request; a missing hash becomes a PersistedQueryNotFoundError. Crucially, if a request arrives with a raw query string but allowArbitraryQueries is false, the plugin throws — this is what makes it an allowlist rather than a cache. The standard APQ flow lets clients register unknown queries; the allowlist deliberately removes that escape hatch.

server wires it together with expressMiddleware, disabling introspection in production and layering the plugin ahead of execution. Note the trade-off: the allowlist requires the client and server manifests to be deployed in lockstep, so a rolling deploy needs both old and new hashes valid simultaneously. Keeping the manifest immutable and versioning it alongside releases avoids the classic pitfall where a freshly deployed client sends hashes the server has not yet learned.


Related snips

Share this code

Here's the card — post it anywhere.

GraphQL persisted queries (hash allowlist) — share card
Link copied