# How Minisearch Implements Search Token Hash Authentication

> Discover how Minisearch implements search token hash authentication using Argon2id for secure API access. Learn about client-side hashing and server-side verification.

- Repository: [Victor Nogueira/minisearch](https://github.com/felladrin/minisearch)
- Tags: internals
- Published: 2026-03-01

---

**Minisearch secures its search API using an Argon2id-based search token hash system where the client derives a proof-of-knowledge hash from a build-time secret, caches it in localStorage to avoid repeated computation, and transmits it via the Authorization header for server-side verification against a filesystem-stored secret.**

Minisearch employs a lightweight yet robust authentication mechanism to protect its search endpoints without exposing sensitive credentials over the wire. This search token hash system combines client-side Argon2id hashing, local caching strategies, and server-side verification with rate limiting to balance security with performance. The implementation spans both the client and server codebase, utilizing specific utilities in [`client/modules/searchTokenHash.ts`](https://github.com/felladrin/minisearch/blob/main/client/modules/searchTokenHash.ts) and [`server/verifyTokenAndRateLimit.ts`](https://github.com/felladrin/minisearch/blob/main/server/verifyTokenAndRateLimit.ts) to create a complete authentication flow.

## Server-Side Secret Generation

The authentication flow begins with a server-side secret that never leaves the filesystem. In [`server/searchToken.ts`](https://github.com/felladrin/minisearch/blob/main/server/searchToken.ts), Minisearch stores a random token in a temporary file named `minisearch-token`. If this file does not exist when the server starts, the system automatically generates a new secret.

```typescript
const getSearchTokenFilePath = () => path.resolve(temporaryDirectory, "minisearch-token");

export const getSearchToken = () => {
  if (!existsSync(getSearchTokenFilePath())) regenerateSearchToken();
  return readFileSync(getSearchTokenFilePath(), "utf8");
};

export function regenerateSearchToken() {
  const newToken = Math.random().toString(36).substring(2);
  writeFileSync(getSearchTokenFilePath(), newToken);
}

```

This secret serves as the password input for all Argon2 operations. Because the token lives only in the server's temporary directory and is never transmitted to clients, the system maintains a strict separation between the secret material and the authentication tokens used in API requests.

## Client-Side Hash Generation

The client derives the search token hash using Argon2id, a memory-hard hashing algorithm resistant to GPU attacks. Located in [`client/modules/searchTokenHash.ts`](https://github.com/felladrin/minisearch/blob/main/client/modules/searchTokenHash.ts), the generation logic uses the `VITE_SEARCH_TOKEN` environment variable (bundled at build time) as the password input.

```typescript
const password = VITE_SEARCH_TOKEN;
const salt = new Uint8Array(16);
crypto.getRandomValues(salt);

const newSearchTokenHash = await argon2id({
  password,
  salt,
  parallelism: 1,
  iterations: 16,
  memorySize: 512,
  hashLength: 8,
  outputType: "encoded",
});

```

The implementation deliberately uses a short 8-byte hash length because the value functions solely as a proof-of-knowledge token rather than a cryptographic key. The Argon2id parameters (16 iterations, 512 KiB memory, single parallelism thread) provide sufficient work-factor protection while remaining computationally feasible for browser environments.

## LocalStorage Caching Strategy

To prevent the client from re-running the expensive Argon2 computation on every search request, Minisearch caches the generated hash in the browser's localStorage. The caching mechanism resides in [`client/modules/pubSub.ts`](https://github.com/felladrin/minisearch/blob/main/client/modules/pubSub.ts) (lines 42-55) and is accessed through the `lastSearchTokenHash` PubSub wrapper.

Before generating a new hash, the client attempts to reuse the cached value:

```typescript
const lastSearchTokenHash = getLastSearchTokenHash();
const lastSearchTokenHashIsValid = await argon2Verify({
  password,
  hash: lastSearchTokenHash,
});

if (lastSearchTokenHashIsValid) return lastSearchTokenHash;

// Otherwise generate fresh hash and cache it
updateLastSearchTokenHash(newSearchTokenHash);

```

This verification step ensures that cached hashes remain valid against the current secret. If the server regenerates its secret token, the cached hash fails verification, triggering the client to compute a new hash automatically.

## Server Verification and Rate Limiting

When a request arrives at the search endpoint, the server extracts the token from the `Authorization` header and validates it through the pipeline defined in [`server/verifyTokenAndRateLimit.ts`](https://github.com/felladrin/minisearch/blob/main/server/verifyTokenAndRateLimit.ts). The verification process employs a two-tier caching strategy to optimize performance.

First, the server checks an in-memory Set of verified tokens managed by [`server/verifiedTokens.ts`](https://github.com/felladrin/minisearch/blob/main/server/verifiedTokens.ts):

```typescript
if (!isVerifiedToken(token)) {
  const isValidToken = await argon2Verify({ 
    password: getSearchToken(), 
    hash: token 
  });
  
  if (!isValidToken) {
    return { isAuthorized: false, statusCode: 401, error: "Invalid token." };
  }
  
  addVerifiedToken(token); // Cache successful verification
}

```

Once verified, the token enters the `verifiedTokens` Set, allowing subsequent requests to skip the computationally expensive Argon2 verification. Finally, a `RateLimiterMemory` instance enforces a quota of 10 requests per 10 seconds per token:

```typescript
await rateLimiter.consume(token);

```

This rate limiting occurs after authentication to prevent brute-force attacks against the Argon2 verification endpoint itself while allowing legitimate users to maintain cached verification status.

## Request Handling Integration

The [`server/handleTokenVerification.ts`](https://github.com/felladrin/minisearch/blob/main/server/handleTokenVerification.ts) module provides a thin HTTP-layer wrapper that translates verification results into proper JSON responses. Search endpoints utilize this handler to enforce authentication before executing queries:

```typescript
const token = req.headers["authorization"]?.toString().split(" ")[1] ?? null;
const { isAuthorized, statusCode, error } = await verifyTokenAndRateLimit(token);

if (!isAuthorized) {
  res.writeHead(statusCode, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ error }));
  return;
}

```

This separation of concerns allows the core verification logic to remain framework-agnostic while providing a standard interface for HTTP response generation.

## Practical Implementation Examples

### Client-Side API Call with Automatic Hash Retrieval

When initiating a search from the client, the application retrieves the cached or newly generated hash and attaches it to the request headers:

```typescript
import { getSearchTokenHash } from "./searchTokenHash";

export async function callSearchApi(query: string) {
  const hash = await getSearchTokenHash();
  const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
    headers: { Authorization: `Bearer ${hash}` },
  });
  return response.json();
}

```

### Server-Side Endpoint Protection

API routes integrate token verification through the dedicated handler:

```typescript
import { handleTokenVerification } from "./handleTokenVerification";
import type { IncomingMessage, ServerResponse } from "http";

export async function searchEndpoint(req: IncomingMessage, res: ServerResponse) {
  const token = req.headers["authorization"]?.toString().split(" ")[1] ?? null;
  const { shouldContinue } = await handleTokenVerification(token, res);
  
  if (!shouldContinue) return;
  
  // Proceed with search implementation
}

```

### Manual Token Regeneration

For development or testing scenarios, you can force a new server secret:

```typescript
import { regenerateSearchToken } from "./searchToken";

regenerateSearchToken(); // Creates new minisearch-token file

```

## Summary

- **Server-side secret**: Stored in a temporary file (`minisearch-token`) and never transmitted to clients.
- **Argon2id hashing**: Client derives 8-byte proof-of-knowledge hashes using memory-hard parameters (16 iterations, 512 KiB).
- **Dual caching**: Client caches hashes in localStorage; server maintains a Set of verified tokens to skip repeated Argon2 computations.
- **Rate limiting**: Each token is restricted to 10 requests per 10 seconds via `RateLimiterMemory`.
- **Zero-trust verification**: Every request validates the hash against the server secret unless previously verified in the current session.

## Frequently Asked Questions

### What hashing algorithm does Minisearch use for search token authentication?

Minisearch uses **Argon2id**, the recommended variant of the Argon2 password-hashing function that provides resistance against both GPU cracking attacks and side-channel timing attacks. The implementation in [`client/modules/searchTokenHash.ts`](https://github.com/felladrin/minisearch/blob/main/client/modules/searchTokenHash.ts) configures Argon2id with 16 iterations, 512 KiB of memory, and single-threaded parallelism to balance security with browser performance.

### How does the client avoid re-computing the hash on every search request?

The client caches the derived Argon2 hash in the browser's **localStorage** through the PubSub wrapper defined in [`client/modules/pubSub.ts`](https://github.com/felladrin/minisearch/blob/main/client/modules/pubSub.ts). Before generating a new hash, the system verifies the cached hash against the secret using `argon2Verify`. If valid, it returns the cached value immediately, avoiding the computational cost of re-hashing until the server secret changes or the cache is cleared.

### What happens when the server regenerates its secret token?

If the server calls `regenerateSearchToken()` or the `minisearch-token` file is deleted, the client-side cached hashes will fail verification against the new secret. The next search request will trigger the `argon2Verify` check in [`searchTokenHash.ts`](https://github.com/felladrin/minisearch/blob/main/searchTokenHash.ts), detect the invalid cache, and automatically generate a new hash using the updated `VITE_SEARCH_TOKEN` value, seamlessly updating the localStorage cache with the valid credentials.

### How does the rate limiting mechanism protect the search API?

The server implements per-token rate limiting using `RateLimiterMemory` configured to allow 10 requests per 10 seconds per unique hash token. This limit is enforced in [`server/verifyTokenAndRateLimit.ts`](https://github.com/felladrin/minisearch/blob/main/server/verifyTokenAndRateLimit.ts) after successful authentication, preventing abuse of the search endpoint while allowing legitimate users with cached verification status to make repeated queries without re-triggering the Argon2 verification overhead.