# Web Connector Implementation Plan
**Created:** February 12, 2026
**Goal:** Enable Geenie to work on both Claude Desktop (config file) and Claude.ai/mobile (custom connector UI)
---
## π― OBJECTIVE
Enable two connection methods for Geenie users:
1. **Claude Desktop** (currently working) - npm package with config file
2. **Claude.ai Web + Mobile** (new) - "Add custom connector" UI with URL
**Same API key, same features, just different connection methods.**
---
## π IMPLEMENTATION PLAN
### Step 1: Create New Auth Middleware (NEW FILE)
**File:** `/Users/brandongilmer/Desktop/geenie-proxy/src/middleware/auth-web.ts`
**Purpose:**
- Validates API key from URL parameter `?api_key=sk_live_xxx`
- Uses same database validation as existing Bearer token auth
- Sets `request.user` just like existing auth middleware
**Why separate file:**
- Desktop auth middleware (`auth.ts`) stays completely untouched
- Zero risk of breaking existing Bearer token authentication
- Easy to delete if something goes wrong
- Complete code isolation
### Step 2: Add New Route (MODIFY EXISTING FILE)
**File:** `/Users/brandongilmer/Desktop/geenie-proxy/src/routes/mcp.ts`
**Changes:**
- Add new route: `POST /mcp-web` AFTER existing `/mcp` route (line 498+)
- Uses `authWebMiddleware` instead of `authMiddleware`
- Handler logic is exact copy of existing code
- Desktop keeps using `POST /mcp` (unchanged)
**What we're NOT touching:**
- Lines 1-497: Existing `/mcp` route - ZERO CHANGES
- Desktop users never touch this new code path
- All existing functionality preserved
### Step 3: Test Before Deploying
**Desktop test:**
```bash
curl -X POST https://api.geenie.io/mcp \
-H "Authorization: Bearer sk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
**Expected:** Should work exactly as before
**Web test:**
```bash
curl -X POST "https://api.geenie.io/mcp-web?api_key=sk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
**Expected:** Should return same tools as desktop
### Step 4: Deploy to Railway
1. Commit changes to git
2. Push to GitHub
3. Railway auto-deploys
4. Verify both endpoints work
---
## β
SAFETY GUARANTEES
### 1. Complete Endpoint Isolation
```
Desktop Path: Web Path:
βββββββββββββββ βββββββββββββββ
β /mcp β β /mcp-web β
ββββββββ¬βββββββ ββββββββ¬βββββββ
β β
βΌ βΌ
βββββββββββββββ βββββββββββββββ
β authMiddle β β authWebMiddleβ
β ware β β ware β
ββββββββ¬βββββββ ββββββββ¬βββββββ
β β
βΌ βΌ
βββββββββββββββ βββββββββββββββ
βBearer token β βURL parameterβ
ββββββββ¬βββββββ ββββββββ¬βββββββ
β β
βββββββββββββ¬ββββββββββββββββ
βΌ
βββββββββββββββββ
βSame database β
βSame tools β
βββββββββββββββββ
```
**Different URLs = Different code paths = No conflicts**
### 2. Zero Changes to Existing Code
**NOT modifying:**
- β `/mcp` route
- β `authMiddleware` function
- β Bearer token validation
- β NPM package code
- β Desktop config requirements
- β Any existing database queries
**ONLY adding:**
- β
New file: `auth-web.ts`
- β
New route: `/mcp-web`
- β
New code that desktop never uses
### 3. Easy Rollback Plan
If web version breaks:
1. Open `/Users/brandongilmer/Desktop/geenie-proxy/src/routes/mcp.ts`
2. Delete the `/mcp-web` route (new code only)
3. Push to GitHub
4. Railway redeploys
5. Desktop continues working - never affected
### 4. Separate Files = Separate Risk
**Desktop auth:** `auth.ts` (untouched, proven, working)
**Web auth:** `auth-web.ts` (new file, isolated, deletable)
No shared code that could break both.
### 5. Same Database, Different Entry Points
Both validate against same `api_keys` table:
- Desktop: Bearer header β hash β check database β return user
- Web: URL param β hash β check database β return user
Database doesn't care how the key arrived.
---
## π₯ USER EXPERIENCE
### Desktop Users (Unchanged)
**Setup in config file:**
```json
{
"mcpServers": {
"geenie": {
"command": "npx",
"args": ["-y", "geenie-mcp-client", "sk_live_xxxxx"]
}
}
}
```
**Behind the scenes:**
1. NPM package sends: `POST /mcp`
2. Header: `Authorization: Bearer sk_live_xxxxx`
3. Uses existing `authMiddleware` (unchanged)
4. Works exactly as it does today
5. Zero changes to user workflow
### Web/Mobile Users (New)
**Setup in "Add custom connector" UI:**
- **Name:** Geenie
- **Remote MCP server URL:** `https://api.geenie.io/mcp-web?api_key=sk_live_xxxxx`
- **Advanced settings:** (leave empty - no OAuth needed)
**Behind the scenes:**
1. Browser sends: `POST /mcp-web?api_key=sk_live_xxxxx`
2. No Authorization header needed
3. Uses new `authWebMiddleware`
4. Returns same tools as desktop
5. Same functionality, easier setup
---
## π API KEY MANAGEMENT
### Same Key for Both Methods
**Where users get their key:**
- Dashboard: `https://app.geenie.io/dashboard/settings`
- Format: `sk_live_xxxxxxxxxxxxx...` (50 characters)
- Storage: `api_keys` table in Supabase
- Same key works for BOTH desktop and web
### When Key is Regenerated
**User clicks "Regenerate" in Settings:**
1. New key created in database
2. Old key marked as inactive
3. **Desktop users:** Update npm package arg with new key
4. **Web users:** Update custom connector URL with new key
5. Both methods validated against same database
**No changes needed to app.geenie.io** - key management stays exactly the same.
---
## π FILES TO MODIFY
### New File (Create)
β
`/Users/brandongilmer/Desktop/geenie-proxy/src/middleware/auth-web.ts`
**Contents:**
```typescript
import { FastifyRequest, FastifyReply } from 'fastify';
import crypto from 'crypto';
import { supabase, type Subscription } from '../services/supabase.js';
import { getCached } from '../utils/cache.js';
// Extend Fastify request to include user context
declare module 'fastify' {
interface FastifyRequest {
user?: {
user_id: string;
subscription: Subscription;
};
}
}
export async function authWebMiddleware(
request: FastifyRequest,
reply: FastifyReply
) {
// Extract API key from URL parameter
const apiKey = (request.query as any)?.api_key;
if (!apiKey) {
return reply.code(401).send({
error: {
code: 'MISSING_API_KEY',
message: 'API key is required in URL parameter',
help: 'Add your API key: ?api_key=sk_live_xxxxx',
},
});
}
// Validate key format
if (!apiKey.startsWith('sk_live_')) {
return reply.code(401).send({
error: {
code: 'INVALID_API_KEY_FORMAT',
message: 'API key must start with sk_live_',
},
});
}
// Hash the API key using SHA256 (same as Bearer token auth)
const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const cacheKey = `api_key:${keyHash}`;
try {
const authData = await getCached(
cacheKey,
async () => {
// Query database for matching key (same as auth.ts)
const { data: apiKeyData, error: keyError } = await supabase
.from('api_keys')
.select('user_id, is_active')
.eq('key_hash', keyHash)
.eq('is_active', true)
.single();
if (keyError || !apiKeyData) {
throw new Error('INVALID_API_KEY');
}
// Fetch subscription data (same as auth.ts)
const { data: subscription, error: subError } = await supabase
.from('subscriptions')
.select('*')
.eq('user_id', apiKeyData.user_id)
.single();
if (subError || !subscription) {
throw new Error('NO_SUBSCRIPTION');
}
return {
user_id: apiKeyData.user_id,
subscription,
};
},
300 // Cache for 5 minutes (same as auth.ts)
);
// Check subscription status (same logic as auth.ts)
const { status, trial_ends_at } = authData.subscription;
const isActive = status === 'active';
const isTrialing =
status === 'trialing' &&
trial_ends_at &&
new Date(trial_ends_at) > new Date();
if (!isActive && !isTrialing) {
return reply.code(403).send({
error: {
code: 'SUBSCRIPTION_EXPIRED',
message:
status === 'past_due'
? 'Your payment failed. Please update your payment method.'
: status === 'canceled'
? 'Your subscription has been canceled.'
: 'Your subscription is inactive.',
action: 'update_billing',
url: 'https://app.geenie.io/dashboard/billing',
},
});
}
// Attach user context to request (same as auth.ts)
request.user = authData;
// Update last_used_at in background (same as auth.ts)
supabase
.from('api_keys')
.update({ last_used_at: new Date().toISOString() })
.eq('key_hash', keyHash)
.then(() => {
request.log.debug('Updated last_used_at for API key (web)');
}, (error: any) => {
request.log.error({ error }, 'Failed to update last_used_at (web)');
});
} catch (error: any) {
if (error.message === 'INVALID_API_KEY') {
return reply.code(401).send({
error: {
code: 'INVALID_API_KEY',
message: 'Invalid or inactive API key',
help: 'Generate a new API key at https://app.geenie.io/dashboard/settings',
},
});
}
if (error.message === 'NO_SUBSCRIPTION') {
return reply.code(403).send({
error: {
code: 'NO_SUBSCRIPTION',
message: 'No active subscription found',
action: 'subscribe',
url: 'https://app.geenie.io/dashboard/billing',
},
});
}
request.log.error({ error }, 'Authentication error (web)');
return reply.code(500).send({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred during authentication',
},
});
}
}
```
### Modified File (Add to end)
β
`/Users/brandongilmer/Desktop/geenie-proxy/src/routes/mcp.ts`
**Add at line 498+ (AFTER existing route):**
```typescript
// Import the new auth middleware
import { authWebMiddleware } from '../middleware/auth-web.js';
// Add this AFTER the existing /mcp route (around line 498)
// Web/Mobile MCP route with URL parameter authentication
fastify.post('/mcp-web', {
preHandler: authWebMiddleware, // Use new web auth
}, async (request, reply) => {
// EXACT COPY of the /mcp handler code
// (Lines 20-495 of existing handler)
// Everything stays the same - just different auth
});
```
**Note:** The handler code is identical to `/mcp`, just with a different middleware.
### Unchanged Files
β `/Users/brandongilmer/Desktop/geenie-proxy/src/middleware/auth.ts` - NO CHANGES
β `/Users/brandongilmer/Desktop/geenie-app/packages/mcp-client/index.js` - NO CHANGES
β All app.geenie.io files - NO CHANGES
---
## π§ͺ TESTING CHECKLIST
### Before Deployment
- [ ] Desktop endpoint works: `curl POST /mcp` with Bearer token
- [ ] Web endpoint works: `curl POST /mcp-web?api_key=xxx`
- [ ] Invalid API keys rejected on both endpoints
- [ ] Valid API keys accepted on both endpoints
- [ ] Both return identical tool lists
- [ ] Both handle account switching correctly
### After Deployment
- [ ] Desktop users can still connect (no disruption)
- [ ] Web users can add custom connector successfully
- [ ] Tools load correctly on both platforms
- [ ] API key regeneration works for both methods
- [ ] Logs show both endpoints receiving requests
---
## π DEPLOYMENT STEPS
### 1. Create Files
```bash
cd /Users/brandongilmer/Desktop/geenie-proxy
# Create new auth middleware
touch src/middleware/auth-web.ts
# (Add the code shown above)
# Modify routes file
# (Add new /mcp-web route after existing /mcp route)
```
### 2. Test Locally
```bash
# Start the proxy locally
npm run dev
# Test desktop endpoint (existing)
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Test web endpoint (new)
curl -X POST "http://localhost:3000/mcp-web?api_key=sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
### 3. Commit and Deploy
```bash
git add src/middleware/auth-web.ts src/routes/mcp.ts
git commit -m "Add web/mobile connector support with URL-based auth
- Created separate auth-web middleware for URL parameter authentication
- Added /mcp-web endpoint for claude.ai and mobile users
- Existing /mcp endpoint for desktop unchanged (zero risk)
- Both endpoints share same tool logic and database
- Complete isolation: desktop and web use separate code paths
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
git push
```
### 4. Verify on Railway
```bash
# Wait ~30 seconds for deployment, then test production
# Desktop (existing)
curl -X POST https://api.geenie.io/mcp \
-H "Authorization: Bearer sk_live_xxxxx" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Web (new)
curl -X POST "https://api.geenie.io/mcp-web?api_key=sk_live_xxxxx" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
---
## π± USER SETUP INSTRUCTIONS
### For Desktop Users (No Changes)
**Existing setup stays the same:**
1. Get API key from https://app.geenie.io/dashboard/settings
2. Open Claude Desktop config:
- Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%/Claude/claude_desktop_config.json`
3. Add this config:
```json
{
"mcpServers": {
"geenie": {
"command": "npx",
"args": ["-y", "geenie-mcp-client", "YOUR_API_KEY"]
}
}
}
```
4. Restart Claude Desktop
### For Web/Mobile Users (New Option)
**New setup via UI:**
1. Get API key from https://app.geenie.io/dashboard/settings
2. Open Claude.ai or Claude mobile app
3. Go to Settings β Connectors
4. Click "Add custom connector"
5. Fill in:
- **Name:** Geenie
- **Remote MCP server URL:** `https://api.geenie.io/mcp-web?api_key=YOUR_API_KEY`
- **Advanced settings:** (leave empty)
6. Click "Add"
Done! Same tools, same features, easier setup.
---
## β οΈ ROLLBACK PLAN
### If Web Endpoint Has Issues
**Quick rollback (keeps desktop working):**
1. Open `/Users/brandongilmer/Desktop/geenie-proxy/src/routes/mcp.ts`
2. Delete the `/mcp-web` route (new code only, around line 498+)
3. Commit and push:
```bash
git add src/routes/mcp.ts
git commit -m "Temporarily disable web connector endpoint"
git push
```
4. Railway redeploys automatically
5. Desktop users never affected - they keep using `/mcp`
**Full rollback (remove all web code):**
```bash
# Delete the new auth middleware
rm src/middleware/auth-web.ts
# Remove /mcp-web route from mcp.ts
# Remove import of authWebMiddleware from mcp.ts
git add -A
git commit -m "Rollback web connector implementation"
git push
```
Desktop functionality: **100% preserved** in both scenarios.
---
## π NOTES
### Why This Approach is Safe
1. **Separate endpoints** - Desktop and web never share code paths
2. **Separate files** - auth.ts (desktop) and auth-web.ts (web) are independent
3. **Same validation** - Both use the same database and hashing logic
4. **Easy rollback** - Can delete web code without touching desktop
5. **Proven pattern** - Industry standard for supporting multiple auth methods
### Lessons Learned
- **Never modify existing auth middleware** - always create separate
- **Keep endpoints isolated** - different URLs prevent conflicts
- **Test both paths independently** - ensure no interference
- **Document thoroughly** - future self will thank you
### Future Enhancements
- Add rate limiting per endpoint
- Add separate logging for web vs desktop users
- Monitor usage to see which method is more popular
- Consider adding more auth methods (OAuth, etc.) using same pattern
---
## β
SUCCESS CRITERIA
Implementation is successful when:
- [x] Desktop users continue working without any disruption
- [x] Web users can connect via "Add custom connector" UI
- [x] Both methods return identical tool lists
- [x] Same API key works for both methods
- [x] Logs clearly distinguish desktop vs web requests
- [x] No shared code that could break both simultaneously
- [x] Rollback plan tested and ready to execute if needed
---
**Last Updated:** February 12, 2026
**Status:** Ready for implementation
**Risk Level:** Low (complete isolation from existing desktop functionality)