diff --git a/.claude/commands/patterns.md b/.claude/commands/patterns.md
deleted file mode 100644
index 24ac88c..0000000
--- a/.claude/commands/patterns.md
+++ /dev/null
@@ -1,90 +0,0 @@
-# Code Patterns
-
-## Error Handling
-
-Express 5 catches async throws. Use the `HttpError` factories:
-
-```typescript
-import { badRequest, unauthorized, notFound, forbidden, conflict } from '../utils/errors.js';
-
-// In any route handler -- no try/catch needed
-throw badRequest('Missing required field');
-throw unauthorized('Token expired');
-throw notFound('App not found');
-```
-
-The error handler in `index.ts` catches these and returns JSON:
-```json
-{ "error": "bad_request", "message": "Missing required field" }
-```
-
-## Database Access
-
-All DB methods are synchronous (better-sqlite3):
-
-```typescript
-// Read
-const app = db.getApp('my-app');
-const client = db.getOIDCClient('client-id');
-const session = db.getSession('session-id');
-
-// Write
-db.upsertApp({ id: 'my-app', name: 'My App', hmac_secret: '...', token_ttl_seconds: 3600 });
-db.upsertOIDCClient({ id: 'client-id', name: 'Client', ... });
-db.createSession({ id: 'uuid', did: 'did:plc:...', handle: '...', app_id: '...', expires_at: new Date() });
-
-// User mappings
-db.setUserMapping({ did: 'did:plc:...', app_id: 'my-app', user_id: 1, handle: 'user.bsky.social' });
-const mapping = db.getUserMapping('did:plc:...', 'my-app');
-
-// Passkeys
-db.savePasskeyCredential({ id: 'cred-id', did: '...', handle: '...', public_key: '...', counter: 0, device_type: 'platform', backed_up: false, transports: null, name: null });
-db.getPasskeyCredential('cred-id');
-db.getPasskeyCredentialsByDid('did:plc:...');
-
-// Audit
-db.logAuditEvent('action', 'actor', 'target', 'details', 'ip');
-```
-
-## Adding OIDC Presets
-
-Edit `src/data/oidc-presets.ts`. Each preset needs:
-
-```typescript
-{
- id: 'my-app',
- name: 'My App',
- icon: ``, // Inline SVG for the setup wizard
- defaultConfig: {
- redirect_uris: (domain: string) => [`https://${domain}/callback`],
- grant_types: ['authorization_code', 'refresh_token'],
- scopes: ['openid', 'profile', 'email'],
- require_pkce: true,
- token_endpoint_auth_method: 'client_secret_basic',
- },
- setup_notes: `Optional markdown instructions shown in the wizard.`,
-}
-```
-
-## Admin Authentication
-
-Two auth methods (both checked by `requireAdmin` middleware):
-
-1. **Bearer token**: `Authorization: Bearer `
-2. **Cookie**: `admin_session` cookie (set via `/admin/login`, 24h TTL, HMAC-signed)
-
-## Proxy Cookie Types
-
-Proxy cookies have a `typ` field to prevent cross-endpoint replay:
-
-- `session` — forward-auth session (long-lived, `/auth/login`)
-- `ticket` — one-time ticket for header exchange (`/auth/verify`)
-
-## HMAC Token Format
-
-`base64url(JSON payload).base64url(HMAC-SHA256 signature)`
-
-Both sides must use UTF-8 encoding of the hex secret string:
-```typescript
-createHmac('sha256', secretString) // NOT Buffer.from(secret, 'hex')
-```
diff --git a/.claude/commands/structure.md b/.claude/commands/structure.md
deleted file mode 100644
index 1b8b545..0000000
--- a/.claude/commands/structure.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Project Structure
-
-```
-gateway/
- src/
- index.ts # Express app setup, middleware, route mounting
- config.ts # Environment config with validation
- routes/
- auth.ts # Legacy HMAC auth flow (/auth/init, /auth/callback)
- admin.ts # Admin API (Bearer + cookie auth)
- admin-dashboard.ts # Server-rendered admin UI + CSRF
- session.ts # Session conflict detection/resolution
- token.ts # HMAC token verify/info
- passkey.ts # WebAuthn registration/authentication routes
- user-profile.ts # User profile page (passkey + session management)
- proxy-auth.ts # Forward-auth routes + enforceAccess
- email.ts # Email verification routes
- mfa.ts # TOTP MFA routes
- oidc/
- index.ts # OIDC route aggregator
- authorize.ts # OIDC authorization + AT Proto OAuth callback
- token.ts # Token exchange (auth_code + refresh_token)
- userinfo.ts # User claims (DID-to-handle resolution)
- revoke.ts # Token revocation (RFC 7009)
- logout.ts # End session endpoint
- discovery.ts # .well-known/openid-configuration + JWKS
- services/
- database.ts # SQLite schema, migrations, all DB methods
- oauth.ts # AT Protocol OAuth client (NodeOAuthClient)
- oidc/ # OIDC token + key services
- passkey.ts # WebAuthn credential management
- email.ts # Email sending (SMTP/SES)
- mfa.ts # TOTP generation/verification
- middleware/
- rateLimit.ts # IP-based rate limiting
- utils/
- hmac.ts # HMAC-SHA256 token creation/verification
- errors.ts # HttpError class + factory functions
- access-check.ts # Handle pattern matching + access control
- proxy-auth.ts # Proxy cookie + ticket helpers
- data/
- oidc-presets.ts # Setup wizard presets (22 apps)
- types/
- index.ts # Shared TypeScript types
- tests/
- oidc-flow.test.ts # E2E OIDC authorization flow tests
- data/ # SQLite database (runtime, gitignored)
-```
-
-## Route Mounting Order (index.ts)
-
-1. `/auth/verify` — forward-auth verify (before rate limit)
-2. Rate limit middleware
-3. `/oauth` — OIDC routes (authorize, token, userinfo, revoke, logout, discovery)
-4. `/auth` — legacy HMAC auth + forward-auth login/callback
-5. `/admin` — admin API + dashboard
-6. `/session` — session management
-7. `/token` — HMAC token verify/info
-8. `/passkey` — WebAuthn routes
-9. `/user` — user profile page
-10. `/email` — email verification
-11. `/mfa` — TOTP MFA
-
-## Key Service Dependencies
-
-- `DatabaseService` — used by all routes
-- `OAuthService` — AT Protocol OAuth, used by auth + OIDC authorize
-- `OIDCService` — token signing/verification, used by OIDC routes
-- `PasskeyService` — WebAuthn, used by passkey + OIDC authorize routes
-- `MFAService` — TOTP, used by MFA routes + admin
-- `EmailService` — email sending, used by email routes
diff --git a/.claude/commands/testing.md b/.claude/commands/testing.md
deleted file mode 100644
index 0be1c64..0000000
--- a/.claude/commands/testing.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Testing Guide
-
-394 tests across 22 test files.
-
-## Commands
-
-```bash
-cd gateway
-npm run test:run # All tests
-npm run test:run -- --coverage # With coverage report
-npm test # Watch mode
-```
-
-## Test Patterns
-
-### Database
-Use in-memory SQLite for isolation:
-```typescript
-const db = new DatabaseService(':memory:');
-afterEach(() => db.close());
-```
-
-### HTTP Routes
-Use supertest with a minimal Express app:
-```typescript
-function createTestApp(db: DatabaseService) {
- const app = express();
- app.use(express.json());
- const router = createMyRouter(db);
- app.use('/path', router);
- // Add error handler for HttpError
- app.use((err: any, _req: any, res: any, _next: any) => {
- res.status(err.statusCode || 500).json({ error: err.code, message: err.message });
- });
- return app;
-}
-```
-
-### Mocking
-- `vi.fn()` for simple function mocks
-- `vi.spyOn(obj, 'method')` for spying on existing methods
-- `vi.mock('module')` for full module mocks (hoisted to top of file)
-
-### OIDC Service Mocks
-```typescript
-function createMockOIDCService(accessTokenClaims: any = null) {
- return {
- tokenService: {
- verifyAccessToken: vi.fn().mockReturnValue(accessTokenClaims),
- verifyIdToken: vi.fn(),
- createTokenResponse: vi.fn(),
- },
- keyService: { getPublicKeySet: vi.fn() },
- } as any;
-}
-```
-
-### WebAuthn Mocks
-Mock the entire `@simplewebauthn/server` module:
-```typescript
-vi.mock('@simplewebauthn/server', () => ({
- generateRegistrationOptions: vi.fn().mockResolvedValue({ challenge: 'mock' }),
- verifyRegistrationResponse: vi.fn().mockResolvedValue({ verified: true, registrationInfo: { ... } }),
- generateAuthenticationOptions: vi.fn().mockResolvedValue({ challenge: 'mock' }),
- verifyAuthenticationResponse: vi.fn().mockResolvedValue({ verified: true, authenticationInfo: { ... } }),
-}));
-```
-
-### Time-Dependent Tests
-```typescript
-vi.useFakeTimers();
-vi.setSystemTime(new Date('2026-01-01'));
-// ... test expiry logic ...
-vi.useRealTimers();
-```
-
-### Registering Test Data
-
-OIDC clients (for revoke, logout, token tests):
-```typescript
-function registerOIDCClient(db: DatabaseService, id = 'test-client', secret = 'test-secret') {
- const secretHash = crypto.createHash('sha256').update(secret).digest('hex');
- db.upsertOIDCClient({
- id, name: 'Test', client_type: 'oidc', hmac_secret: secretHash,
- redirect_uris: ['https://app.example.com/callback'],
- grant_types: ['authorization_code'], allowed_scopes: ['openid', 'profile'],
- token_ttl_seconds: 3600, id_token_ttl_seconds: 3600,
- access_token_ttl_seconds: 3600, refresh_token_ttl_seconds: 86400,
- require_pkce: false, token_endpoint_auth_method: 'client_secret_basic',
- });
-}
-```
-
-Legacy apps (for session, token tests):
-```typescript
-function registerApp(db: DatabaseService, id = 'test-app') {
- db.upsertApp({
- id, name: 'Test App', hmac_secret: TEST_SECRET,
- token_ttl_seconds: 3600, callback_url: 'https://app.example.com/callback',
- });
-}
-```
-
-## File Organization
-
-- Tests live alongside source: `foo.ts` -> `foo.test.ts`
-- E2E tests in `tests/` directory
-- No test utilities directory -- helpers are defined per test file
-
-## Coverage
-
-Run `npm run test:run -- --coverage` and check the terminal table.
-Key areas with room for improvement: passkey routes, OIDC authorize, OIDC token exchange.
diff --git a/.gitignore b/.gitignore
index e4859ad..12663c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,10 @@ gateway/node_modules/
gateway/dist/
gateway/.turbo/
+# Claude Code
+CLAUDE.md
+.claude/
+
# IDE
.idea/
.vscode/
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 0051053..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# ATAuth - AT Protocol OIDC Provider
-
-OIDC Provider using AT Protocol OAuth (Bluesky) as identity source, plus a forward-auth SSO proxy for nginx `auth_request`.
-
-## Development
-
-```bash
-cd gateway
-npm install
-cp .env.example .env # Fill in required secrets (openssl rand -hex 32)
-npm run dev # tsx watch (hot reload)
-```
-
-| Command | Description |
-|---------|-------------|
-| `npm run dev` | Hot reload (tsx watch) |
-| `npm run build` | TypeScript compile |
-| `npm run test:run` | Vitest single run |
-| `npm run test:run -- --coverage` | With coverage |
-| `npm run typecheck` | tsc --noEmit |
-| `npm run lint` | ESLint |
-
-## Stack
-
-Express 5, TypeScript, SQLite (better-sqlite3), ES256 JWTs (OIDC), HMAC-SHA256 (gateway/proxy tokens).
-
-## Gotchas
-
-- `req.accepts('json')` matches `*/*` -- use `req.is('json')` to check Content-Type
-- `/auth/verify` is mounted before rate limit middleware (called on every nginx subrequest)
-- HMAC tokens: both sides must use UTF-8 encoding of the hex secret string
-- Proxy cookies use a `typ` discriminator to prevent cross-endpoint replay
-- OIDC issuer URL must exactly match what clients configure (including path)
-- Client secrets stored as SHA-256 hashes, never plaintext
-
-## Skills
-
-- `/structure` -- project file layout, route mounting order, service dependencies
-- `/testing` -- test patterns, mock examples, coverage, helper recipes
-- `/patterns` -- error handling, DB access, OIDC presets, auth, HMAC format
diff --git a/README.md b/README.md
index d80edaa..ea49495 100644
--- a/README.md
+++ b/README.md
@@ -154,7 +154,6 @@ With a self-hosted PDS, ATAuth becomes a fully independent auth system:
## Documentation
-- [Development Guide](CLAUDE.md)
- [Homelab Deployment Guide](docs/HOMELAB.md)
- [Security Policy](SECURITY.md)
- [Changelog](CHANGELOG.md)