# How Agent API Keys Work with Hash-Based Storage and Company Boundaries in Paperclip

> Discover how Paperclip's agent API keys use SHA-256 hashing for secure storage and automatically enforce company boundaries with `companyId` scoping. Learn more now.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: internals
- Published: 2026-08-16

---

**Agent API keys in Paperclip are stored as SHA‑256 hashes (never plain text) and automatically scope every request to a specific company by attaching `companyId` to the request actor.**

Paperclip's agent API key system combines cryptographic hashing with strict tenant isolation. This article explains how the open-source `paperclipai/paperclip` repository implements secure key storage and enforces company boundaries at every layer of the request lifecycle.

## How Agent API Keys Are Created and Hashed

When you create an agent API key, Paperclip never stores the actual secret. Instead, the `createKey` service in [`server/src/services/agents.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/services/agents.ts) performs a one-way transformation:

```ts
// server/src/services/agents.ts – createKey
const token = createToken();                         // random secret
const keyHash = hashToken(token);                    // SHA‑256 hash
await db.insert(agentApiKeys).values({
  agentId: id,
  companyId: existing.companyId,
  name,
  keyHash,
  responsibleUserId: options?.responsibleUserId?.trim() || null,
  scopeConfig: scope.kind === "standard" ? null : scope,
});
return { token, /* other metadata */ };

```

The `hashToken` function applies SHA‑256 to the random token. Only the resulting `keyHash` is persisted to the `agent_api_keys` table defined in [`packages/db/src/schema/agent_api_keys.ts`](https://github.com/paperclipai/paperclip/blob/main/packages/db/src/schema/agent_api_keys.ts). The original token is returned to the client exactly once—if lost, it cannot be recovered.

## Authenticating Requests with Hash-Based Lookup

The `actorMiddleware` in [`server/src/middleware/auth.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/middleware/auth.ts) handles every incoming request bearing an `Authorization: Bearer <token>` header. It re-hashes the incoming token and performs a constant-time lookup:

```ts
// server/src/middleware/auth.ts – actorMiddleware
const token = authHeader.slice("bearer ".length).trim();
const tokenHash = hashToken(token);
const key = await db
  .select()
  .from(agentApiKeys)
  .where(and(eq(agentApiKeys.keyHash, tokenHash), isNull(agentApiKeys.revokedAt)))
  .then(rows => rows[0] ?? null);

if (!key) { /* fallback to JWT or reject */ }

```

This design ensures the secret token never traverses the network after creation. A leaked database dump exposes only irreversible hashes, rendering the keys useless to attackers.

## Enforcing Company Boundaries Through Actor Scoping

Once a key is authenticated, Paperclip binds the request to a specific tenant. The middleware populates `req.actor` with the `companyId` stored alongside the key:

```ts
// server/src/middleware/auth.ts
req.actor = {
  type: "agent",
  agentId: key.agentId,
  companyId: key.companyId,
  keyId: key.id,
  keyScope: normalizeAgentApiKeyScope(key.scopeConfig),
  onBehalfOfUserId: normalizeOptionalString(key.responsibleUserId),
  // …
};

```

All downstream queries automatically filter by `req.actor.companyId`:

```ts
// Any service that queries resources
await db.select().from(someTable)
  .where(eq(someTable.companyId, req.actor.companyId));

```

This guarantees that an agent key issued for Company A can never access Company B's resources, even if the key hash is somehow compromised and replayed.

## Responsible User Requirements and Audit Logging

Paperclip enforces traceability by requiring every agent key to have a `responsibleUserId`. If this field is missing, the request is rejected and audited:

```ts
// server/src/middleware/auth.ts
if (!key.responsibleUserId) {
  await auditAgentKeyMissingResponsibleUser(...);
  throw forbidden("Responsible user is unavailable for this agent key");
}

```

The `auditAgentKeyMissingResponsibleUser` function logs the event for security review. This prevents anonymous agent actions and maintains accountability across company boundaries.

## Key Revocation and Usage Tracking

Each authentication attempt updates `lastUsedAt` on the key record:

```ts
await db.update(agentApiKeys)
  .set({ lastUsedAt: new Date() })
  .where(eq(agentApiKeys.id, key.id));

```

Keys can be revoked by setting `revokedAt` to a timestamp. The authentication query explicitly checks `isNull(agentApiKeys.revokedAt)`, ensuring revoked keys immediately fail validation.

## Summary

- **Hash-based storage** — Agent API keys are SHA‑256 hashed before storage; only hashes touch the database.
- **One-way secrets** — Original tokens are generated once and never retrievable; loss requires key rotation.
- **Company scoping** — Every authenticated request carries `companyId` in `req.actor`, enforcing tenant isolation automatically.
- **Responsible user enforcement** — Missing `responsibleUserId` triggers 403 Forbidden and audit logging.
- **Operational visibility** — `lastUsedAt` tracking and `revokedAt` checks support key lifecycle management.

## Frequently Asked Questions

### How does Paperclip prevent database leaks from exposing agent API keys?

Paperclip stores only SHA‑256 hashes of tokens in the `keyHash` column. The original random token is returned to the client once during creation and discarded. Because the hash function is one-way, an attacker with database access cannot reverse the hashes to obtain usable tokens.

### What happens if an agent API key is used without a responsible user?

The `actorMiddleware` checks `key.responsibleUserId` immediately after authentication. If null, it calls `auditAgentKeyMissingResponsibleUser` and throws a 403 Forbidden error. This ensures every agent action is traceable to a human user within the company.

### Can an agent API key from one company access another company's data?

No. The `companyId` field is stored with each key and injected into `req.actor.companyId` during authentication. All database queries filter by this value, making cross-tenant access impossible regardless of key compromise or misconfiguration.

### Where is the token hashing function implemented in Paperclip?

The `hashToken` function is defined in [`server/src/middleware/auth.ts`](https://github.com/paperclipai/paperclip/blob/main/server/src/middleware/auth.ts) at lines 53–55. It uses SHA‑256 to transform the bearer token into a lookup key for the `agent_api_keys` table.