--- title: "ADR 0002 — Bearer tokens, not OAuth" type: decision tags: [signpost, adr, decision] proposed_by: tok_bc09b340db294c709763789154d8113d model: kmem-docs-publisher extracted_at: "2026-10-08T22:37:11.191Z" --- # ADR 0002 — Bearer tokens, not OAuth **Status:** superseded by 0019 **Date:** 2026-08-08 ## Decision The MCP server and the Quick Connect endpoints authenticate with a bearer token: `Authorization: Bearer sp_...`, validated against the `session_tokens` table. Quick Connect also accepts the token as a path segment, because a URL is the only thing you can paste into a chat window. Only the SHA-256 of a token is stored. The raw token is shown once, at mint, and is not recoverable. There are exactly two scopes, `read` and `write`, and `write` implies `read`. ## Alternative rejected OAuth 2.1 with dynamic client registration, which is what the MCP specification prefers for remote servers. ## Why OAuth buys three things: a consent screen, per-client credentials, and revocation without sharing a secret. For a single-project self-hosted instance, the first is ceremony between a person and their own server, the second is solved by minting a second token, and the third is solved by a `revoked_at` column. Against that: OAuth with DCR is an authorisation server, a token endpoint, a registration endpoint, refresh-token rotation, PKCE, client storage, and a consent UI. It is roughly a week of work, plus a permanent maintenance surface, plus a class of bugs — redirect URI validation, token substitution, confused deputy — that a bearer token does not have. And it buys nothing for adoption. Claude web, Claude desktop, Claude Code, ChatGPT, Cursor and Codex all accept bearer-token or authless remote MCP servers today. The paste-a-link door — the thing that makes Signpost usable from a chat window with no install at all — cannot use OAuth even in principle. ## Consequences - **The URL is the credential.** This is stated plainly in the README, in the `AGENTS.md` briefing the model reads, and in the token mint dialog. URLs leak into transcripts, history and screenshots. - Mitigations are mandatory rather than optional: default seven-day expiry, per-token rate limiting (60/min, 1000/hour), instant revocation effective within one request, an audit-log entry per request with label, IP and user-agent, and a line in the briefing telling the model never to echo the URL. - Scope granularity beyond read/write is deliberately absent. Finer permissions are a hosted-tier feature; adding them here would mean a policy engine for an instance with one project in it. - A token is a bearer credential with no user identity attached, so the audit log records the token *label*, not a person. Teams that need per-person attribution use the web app, which authenticates with `better-auth` sessions. - If the MCP ecosystem consolidates on OAuth in a way that excludes bearer tokens, this decision gets a superseding ADR. The `session_tokens` table and the scope model survive that change; only the front door moves.