Impact Flow: Auth & User Tracking Plan

OAuth Edition — Google Sign-In + API Key for MCP

Why OAuth over Password Auth

ConcernPassword (JWT-only)OAuth (Google)
Password storageYou manage bcrypt hashesGoogle handles it
Password reset flowYou build itNot needed
Email verificationYou build itGoogle verifies
Brute-force protectionYou implement rate limitingGoogle handles it
Account recoveryYou build itGoogle handles it
Initial setupNo external depsRegister Google Cloud app (~15 min)
Ongoing maintenanceHighNear zero
Google OAuth 2.0 authlib FastAPI SQLite API Key (MCP)
1 Google OAuth + Simplified User Model ~1 day
1.1 Google Cloud Console setup
  • Go to console.cloud.google.com → APIs & Services → Credentials
  • Create an OAuth 2.0 Client ID (Web application)
  • Authorized redirect URI: http://100.103.206.4:8000/api/auth/callback
  • Also add http://localhost:8000/api/auth/callback for local dev
  • Save the Client ID and Client Secret
Store these in .env, never commit them. Add .env to .gitignore if not already there.
GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxx JWT_SECRET=generate-a-random-64-char-string IMPACTFLOW_API_KEY=generate-another-random-64-char-string
1.2 Simplified users table (no passwords!)
CREATE TABLE users ( id TEXT PRIMARY KEY, -- UUID email TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL, avatar_url TEXT, -- from Google profile google_id TEXT UNIQUE NOT NULL, -- Google sub claim role TEXT DEFAULT 'user', -- user | admin created_at TEXT DEFAULT (datetime('now')), last_login_at TEXT );
No password_hash, no password reset, no email verification. Google handles all of that.

Files: app/db.py app/models.py

1.3 Install dependencies
pip install authlib httpx python-jose[cryptography]

authlib handles the OAuth dance. python-jose for JWT signing. No bcrypt/passlib needed.

1.4 OAuth flow — 3 endpoints
from authlib.integrations.starlette_client import OAuth oauth = OAuth() oauth.register( name="google", client_id=os.getenv("GOOGLE_CLIENT_ID"), client_secret=os.getenv("GOOGLE_CLIENT_SECRET"), server_metadata_url="https://accounts.google.com/.well-known/openid-configuration", client_kwargs={"scope": "openid email profile"}, )

GET /api/auth/login — Redirects browser to Google consent screen

@app.get("/api/auth/login") async def login(request: Request): redirect_uri = request.url_for("auth_callback") return await oauth.google.authorize_redirect(request, redirect_uri)

GET /api/auth/callback — Google redirects back here with auth code

@app.get("/api/auth/callback") async def auth_callback(request: Request): token = await oauth.google.authorize_access_token(request) user_info = token.get("userinfo") # Find or create user user = db.get_user_by_google_id(user_info["sub"]) if not user: user = db.create_user( email=user_info["email"], display_name=user_info["name"], avatar_url=user_info.get("picture"), google_id=user_info["sub"], ) # Update last login db.update_last_login(user.id) # Issue our own JWT access_token = create_jwt({"sub": user.id, "email": user.email}) return {"access_token": access_token, "token_type": "bearer", "user": user}

GET /api/auth/me — Returns current user from JWT

@app.get("/api/auth/me") async def get_me(user = Depends(get_current_user)): return user

Files: app/auth.py (new) app/main.py

1.5 Dual auth: JWT + API Key

The auth dependency accepts either a valid JWT or the X-API-Key header. This keeps the MCP server working without OAuth.

async def get_current_user( request: Request, credentials: Optional[HTTPAuthorizationCredentials] = Depends( HTTPBearer(auto_error=False) ), db = Depends(get_db), ) -> User: # Check API key first (for MCP server) api_key = request.headers.get("x-api-key") if api_key and api_key == os.getenv("IMPACTFLOW_API_KEY"): return db.get_admin_user() # API key acts as admin # Then check JWT (for browser users) if credentials: payload = decode_jwt(credentials.credentials) user = db.get_user(payload["sub"]) if user: return user raise HTTPException(status_code=401, detail="Not authenticated")
MCP server just adds X-API-Key: {key} header to every request. No OAuth dance needed for machine-to-machine.
1.6 Session middleware (for OAuth state)

authlib needs Starlette sessions to store the OAuth state parameter during the redirect flow. Add:

from starlette.middleware.sessions import SessionMiddleware app.add_middleware(SessionMiddleware, secret_key=os.getenv("JWT_SECRET"))
This is only for the OAuth redirect flow. Your API auth still uses stateless JWTs, not session cookies.
2 Protect Endpoints & Multi-User Data ~0.5 day
2.1 Add user_id to all data tables
ALTER TABLE projects ADD COLUMN user_id TEXT REFERENCES users(id); ALTER TABLE visions ADD COLUMN user_id TEXT REFERENCES users(id); ALTER TABLE journal_entries ADD COLUMN user_id TEXT REFERENCES users(id); CREATE INDEX idx_projects_user ON projects(user_id); CREATE INDEX idx_visions_user ON visions(user_id); CREATE INDEX idx_journal_user ON journal_entries(user_id);
2.2 Migration script

One-time migration that preserves all existing data:

  • Create users table
  • Create your admin user (first Google login auto-promotes to admin)
  • Add user_id columns
  • Backfill existing rows with admin user_id
  • Add indexes
-- Auto-promote first user to admin -- In auth_callback, if no users exist yet: if db.count_users() == 0: user.role = "admin"

Files: app/migrations/001_add_auth.py (new)

2.3 Scope all queries

Every CRUD operation filters by the authenticated user:

# Before (no auth) def get_projects(db): return db.execute("SELECT * FROM projects") # After (user-scoped) def get_projects(db, user_id: str): return db.execute( "SELECT * FROM projects WHERE user_id = ?", [user_id] )
  • GET list endpoints: filter by user_id
  • GET detail endpoints: filter by user_id (prevents accessing other users' data)
  • POST: auto-set user_id from authenticated user
  • PUT/PATCH/DELETE: verify ownership before modifying

Files: app/db.py app/main.py

2.4 Route classification
RouteAuthNotes
/healthPublicHealth check
/api/auth/loginPublicInitiates OAuth
/api/auth/callbackPublicOAuth callback
/api/auth/meProtectedCurrent user profile
/api/projects/*ProtectedUser-scoped
/api/visions/*ProtectedUser-scoped
/api/journal/*ProtectedUser-scoped
3 Activity Tracking ~1 day
3.1 Activity log table
CREATE TABLE activity_log ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), action TEXT NOT NULL, -- create|update|delete|view|login|export resource TEXT NOT NULL, -- project|vision|journal|auth resource_id TEXT, -- FK to the affected record metadata TEXT, -- JSON for extra context source TEXT DEFAULT 'web', -- web|mcp|api ip_address TEXT, user_agent TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX idx_activity_user ON activity_log(user_id, created_at DESC); CREATE INDEX idx_activity_resource ON activity_log(resource, resource_id);
3.2 Tracking middleware
@app.middleware("http") async def track_activity(request: Request, call_next): response = await call_next(request) user = getattr(request.state, "user", None) if user and response.status_code < 400: # Determine source source = "mcp" if request.headers.get("x-api-key") else "web" log_activity( user_id=user.id, action=method_to_action(request.method), resource=path_to_resource(request.url.path), source=source, ip=request.client.host, ua=request.headers.get("user-agent", ""), ) return response def method_to_action(method: str) -> str: return {"GET": "view", "POST": "create", "PUT": "update", "PATCH": "update", "DELETE": "delete"}.get(method, "other")

Files: app/tracking.py (new)

3.3 What to track
  • Auth events: login (via OAuth callback), failed attempts
  • CRUD ops: create/update/delete on projects, visions, journal
  • Detail views: GET on individual records (not list endpoints)
  • MCP calls: tagged with source="mcp" to distinguish Claude-initiated actions
  • Exports: any data export or report generation
3.4 Query endpoints
GET /api/activity?page=1&limit=20 -- Paginated activity feed for current user GET /api/activity/summary?days=30 -- Returns: { actions_per_day, top_resources, mcp_vs_web_ratio } GET /api/admin/activity?user_id=... (admin only) -- All users' activity
3.5 Retention

Auto-prune logs older than 90 days. Run on app startup:

@app.on_event("startup") async def prune_old_activity(): db.execute(""" DELETE FROM activity_log WHERE created_at < datetime('now', '-90 days') """)
4 Discovery & Polish ~1 day
4.1 User profile endpoints
  • GET /api/me — full profile + stats
  • PATCH /api/me — update display_name (email managed by Google)
  • GET /api/me/stats — personal usage: projects created, journal streaks, active days
4.2 Token management
CREATE TABLE refresh_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id), token_hash TEXT NOT NULL, device TEXT, -- from user-agent created_at TEXT DEFAULT (datetime('now')), expires_at TEXT NOT NULL );
  • GET /api/me/sessions — list active refresh tokens (device, created)
  • DELETE /api/me/sessions/{id} — revoke a token
  • Issue refresh token on OAuth callback, access token from refresh
4.3 Update MCP server
  • Add X-API-Key header to every request in MCP server config
  • New MCP tool: get_activity_summary — "What have I been working on?"
  • New MCP tool: get_recent_changes — Feed of what changed recently
  • New MCP tool: get_user_profile — Current user info and stats

Files: mcp_server/tools.py

4.4 Security hardening
  • CORS: restrict origins to Tailscale IP + localhost
  • Rate limit OAuth callback (prevent abuse)
  • Allowed email domains: optionally restrict to specific domains
  • JWT expiry: 15 min access, 7 day refresh
  • All secrets in .env, .env in .gitignore
4.5 What you no longer need to build
Removed from scope (Google handles these):
POST /api/auth/registerPassword hashing (bcrypt)Password reset flowEmail verificationBrute-force protection on loginAccount recoverypasslib dependency
4.6 Future considerations
  • Additional OAuth providers: GitHub, Microsoft — authlib makes adding providers trivial (same pattern, new registration)
  • Invite system: Admin generates invite links, new users must have invite to create account
  • Team/org model: Shared projects between users (if Impact Flow grows beyond personal use)
  • Allowed domains: Restrict sign-up to @computerim.com emails