Trovella Wiki

Personal Access Tokens

PAT lifecycle, the trov_ prefix convention, SHA-256 hashing, and how MCP requests authenticate without sessions.

Personal Access Tokens (PATs) authenticate MCP requests from tools like Claude Code. They are long-lived bearer tokens that bypass the session/cookie system entirely. PATs identify the user; the MCP server resolves the tenant from context.

Token Format

Every PAT follows this format:

trov_<40 hex characters>

Total length: 45 characters. The trov_ prefix makes tokens identifiable in logs, environment variables, and secret scanners. The 40 hex characters come from 20 cryptographically random bytes (randomBytes(20).toString("hex")).

Lifecycle

Creation

Users create PATs through the tRPC pat.create endpoint:

  1. The client sends a name (required, 1-100 chars) and optional expiresInDays (1-365)
  2. The server generates a random token with trov_ prefix
  3. The token is SHA-256 hashed using the Web Crypto API (crypto.subtle.digest)
  4. The hash, a prefix for display (trov_XXXX), name, user ID, and optional expiration are stored in the personal_access_token table
  5. The raw token is returned to the client once -- it cannot be retrieved again
const rawToken = generateToken(); // trov_ + 40 hex chars
const tokenHash = await hashToken(rawToken); // SHA-256
await createPAT({ id, userId, name, tokenHash, tokenPrefix: rawToken.slice(0, 9), expiresAt });
return { id, token: rawToken }; // shown once

Validation

MCP requests authenticate via the authenticateRequest() function in packages/mcp/src/auth.ts:

  1. Extract the Authorization: Bearer <token> header
  2. Verify the token starts with trov_
  3. SHA-256 hash the raw token
  4. Query personal_access_token joined to user where tokenHash matches and revokedAt is null
  5. Check expiresAt (if set) is in the future
  6. Fire-and-forget update to lastUsedAt for monitoring
  7. Return { userId, userName, userEmail } or null

The validation performs a constant-time comparison via the database lookup (the hash is the lookup key, not the raw token).

Revocation

Users revoke PATs through the pat.revoke endpoint. Revocation is a soft delete -- revokedAt is set to the current timestamp. The token row is preserved for audit purposes but excluded from validation queries (WHERE revoked_at IS NULL).

Only the owning user can revoke their tokens. The query filters on both userId and token id.

Listing

The pat.list endpoint returns all non-revoked PATs for the current user, showing name, tokenPrefix (first 9 chars for identification), lastUsedAt, expiresAt, and createdAt. The full token hash is never returned to the client.

Schema

The personal_access_token table is defined in packages/db/src/schema/mcp.ts:

ColumnTypePurpose
idTEXT (PK)UUID generated at creation
userIdTEXT (FK to user)Token owner
nameTEXTUser-provided label
tokenHashTEXT (unique)SHA-256 hash of raw token
tokenPrefixTEXTFirst 9 chars for display (trov_XXXX)
lastUsedAtTIMESTAMPUpdated on each successful validation
expiresAtTIMESTAMPOptional expiration
revokedAtTIMESTAMPSoft-delete marker
createdAtTIMESTAMPCreation time

Indexes: pat_userId_idx on userId, pat_tokenHash_idx on tokenHash.

Why PATs Are User-Scoped, Not Tenant-Scoped

PATs identify who is making the request, not which tenant they are operating on. When an MCP request arrives:

  1. The PAT resolves the user
  2. The MCP server determines the active tenant from context (the user's org membership)
  3. withTenantContext() applies RLS for that tenant

This design means a single PAT works across all organizations the user belongs to. The alternative -- tenant-scoped tokens -- would require users to manage one token per organization and reconfigure their MCP client when switching.

Because PATs are user-scoped, the PAT tRPC router uses protectedProcedure (requires auth, no org context) instead of authorizedProcedure (requires auth + org + CASL). The personal_access_token table has no organization_id column and no RLS policies.

Security Model

ConcernMitigation
Token storageOnly SHA-256 hash stored; raw token shown once at creation
Token leakagetrov_ prefix enables secret scanners; lastUsedAt reveals unauthorized usage
Token compromiseOptional expiration (1-365 days); instant revocation
Brute force40 hex chars = 160 bits of entropy; SHA-256 hashing adds computational cost
Scope creepPATs grant the user's full permissions (no fine-grained scopes); see risks below

Known Risk: No Scope Restrictions

PATs currently grant the same access as a session cookie -- full API access for the user. Fine-grained scopes (e.g., read-only, specific-org) are deferred. This is acceptable for the MVP where the only PAT consumer is Claude Code operating with the user's full intent.

OAuth 2.1 (Deferred)

The MCP specification supports OAuth 2.1, which is the proper long-term solution for Claude Web/Chat/Work integration. OAuth 2.1 provides short-lived tokens with refresh capability, reducing the blast radius of a leaked token. This is tracked in TRO-48 and deferred to post-MVP. PATs are simpler, immediately reliable, and sufficient for Claude Code's HTTP transport.

On this page