Skip to content

Remove Convex: Flask + SQLite + SSE becomes the only backend#15

Merged
AdamEXu merged 11 commits into
devfrom
worktree-remove-convex
Jul 8, 2026
Merged

Remove Convex: Flask + SQLite + SSE becomes the only backend#15
AdamEXu merged 11 commits into
devfrom
worktree-remove-convex

Conversation

@AdamEXu

@AdamEXu AdamEXu commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Removes Convex entirely. Flask + SQLite is now the single source of truth, and a new per-user SSE event channel (GET /api/events, Redis Streams over the same Upstash infra as chat token streaming) preserves full live-update parity — every surface that updated reactively under Convex stays push-driven: onboarding auto-advance, thread list reordering, generation/cancel state across tabs, message visibility, upcoming refresh, profile picture.

Design doc: MIGRATION_SPEC.md (committed at repo root).

What changed

Backend

  • db/chat_store.py — ports the Convex chat state machine (transition guards, idempotency via UNIQUE(user_id, client_request_id), terminal no-ops, BEGIN IMMEDIATE in place of OCC) to a new chat.db; Convex _id strings preserved so old /chat/#<id> URLs keep working
  • db/app_users.py + columns on users — onboarding/consent/profile state (was Convex users table); chat entitlement now enforced server-side
  • db/schoology_cache_store.py — ports schoologyCache.ts diff/tombstone logic (batched queries)
  • services/events.py + events/ blueprint — the app-wide SSE channel; stores publish typed events after commit so no call site can forget
  • chat/ blueprint + services/chat/launcher.pyPOST /api/chat/messages replaces the sendMessage → Convex action → internal-secret hop (one hop instead of three); reaper.py daemon thread replaces the failStaleGenerations cron and now also terminates attached SSE clients, preserving partial streamed text
  • Deleted: services/convex_bridge.py, all three convex_sync.py bridges, internal_chat/, the aud=convex JWT/JWKS surface, CONVEX_*/CHAT_INTERNAL_SECRET config
  • Chat token streaming (Redis + SSE) is unchanged — it never used Convex

Frontend

  • hooks/useAppEvents.ts (singleton SSE reader, Last-Event-ID reconnect) + hooks/useLiveQuery.ts (fetch-on-mount, apply-on-event, focus/reconnect resync, visibility-aware backstop poll) + lib/api.ts
  • Chat page rewritten to a single-SSE-source model; streamed text is kept on terminal instead of waiting for a reactive re-push (removes the old flicker window)
  • Deleted: frontend/convex/, ConvexClientProvider, 4 dead hooks, the convex dependency

Migration

  • backend/scripts/migrate_convex_export.py — one-time import of a npx convex export zip (chat history, user state, preferences; schoology cache deliberately self-heals via refresh). Run once against prod data before deploying: python -m scripts.migrate_convex_export <export.zip>

Verification

  • python -m compileall clean; python -m scripts.smoke_stores — 36 assertions over the ported state machine (idempotency, guards, reaper, tombstoning) all pass
  • pnpm build clean; zero convex references left in frontend/src
  • High-effort multi-agent code review over the full diff; all confirmed findings fixed in the final commit
  • Not exercised here (no Schoology account/Redis in the sandbox): the §8.4 manual smoke checklist in MIGRATION_SPEC.md — worth a pass before merging, especially chat resume/cancel across tabs and onboarding advance

Post-merge follow-ups

  • Update ~/Developer/one/CLAUDE.md (outside this repo) — still documents the Convex architecture
  • Env cleanup per MIGRATION_SPEC.md appendix (delete CONVEX_URL, CONVEX_BRIDGE_SECRET, CHAT_INTERNAL_SECRET, NEXT_PUBLIC_CONVEX_URL; optionally set APP_EVENTS_*)
  • Known accepted residuals: scraper's last_convex_check_at column name (schema, out of scope), CHAT_CONVEX_HEARTBEAT_MS env fallback (intentional back-compat)

🤖 Generated with Claude Code

AdamEXu and others added 7 commits July 7, 2026 13:18
…ript (P1+P7)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the chatInternal.ts state machine (transition guards, idempotency,
COALESCE fallbacks, stale-generation reaper) to backend/db/chat_store.py,
user app-state to app_users.py, and schoologyCache.ts diff/tombstone logic
to schoology_cache_store.py, with a permanent smoke gate in scripts/smoke_stores.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chat generation now writes directly to SQLite via chat_store (launcher
replaces the internal_chat blueprint, reaper thread replaces the Convex
cron); schoology/user/auth routes read and write local stores; the
frontend replaces Convex reactive queries with useLiveQuery over the
/api/events SSE channel; chat page keeps streamed text on terminal
instead of waiting for a reactive re-push. Deletes frontend/convex/,
the bridge layers, and the aud=convex JWT surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_to_cache (P8)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c and polling

- Order messages by insertion (rowid) so a turn's user prompt always
  precedes its assistant reply; migration sorts by _creationTime
- Publish chat.thread.updated on existing-thread send so the sidebar
  reorders instantly in every tab
- Preserve partial streamed text when the reaper terminates a stale
  generation; skip the write lock on idle reaper ticks
- Dedupe the optimistic user bubble against its own message event;
  resync messages and active-generation state after SSE reconnect
- Batch N+1 course queries; stop background-tab polling and double
  focus refetches; drop stale convex dep from nested requirements

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Redis-snapshot-only check fails open when the snapshot is absent (LRU
eviction, post-terminal state), streaming a generation to any authenticated
user. Check the authoritative owner in chat.db before streaming — restores
the fail-closed guarantee PR #4 provided via Convex, now against the new
SQLite source of truth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integrate the Convex removal with everything on dev (#1-#14). Resolutions:
- api/routes.py: keep #15's fail-closed SQLite ownership check (restores PR
  #4's IDOR guarantee against the new source of truth).
- services/schoology/client.py: graft PR #3's full SSRF hardening
  (validate_outbound_url + per-hop re-validating redirect loop) onto #15's
  rewritten client, which only had host-scoped auth.
- auth/routes.py: keep PR #8's log-injection fix (log arg keys, not values).
- Convex modules/frontend deleted by #15 (convex_sync, internal_chat,
  convex/, ConvexClientProvider): keep deleted.
- chat/page.tsx: take #15's SSE rewrite, which already re-implements PR #5's
  input-preservation and loading-vs-not-entitled behavior. Tool-call
  rehydration on reconnect remains a known follow-up (needs an SSE-era
  endpoint).
- tests/test_jwt_utils.py: drop tests for the Convex JWT audience and JWKS
  document #15 removed; algorithm-confusion coverage retargeted to the
  mobile audience, not deleted.

Verified on the merged tree: ruff clean, 142 passed + 2 xpassed, smoke_stores
36/36, app imports clean, frontend tsc --noEmit clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AdamEXu AdamEXu changed the base branch from main to dev July 8, 2026 00:41
Comment thread backend/db/app_users.py

def _fetch_user_row(executor, user_id: int):
return executor.execute(
f"SELECT {_USER_COLUMNS} FROM users WHERE id = ?", (user_id,)
Comment thread backend/db/app_users.py
conn = get_conn(Config.MAIN_DB_PATH)
cursor = conn.cursor()
cursor.execute(
f"UPDATE users SET {assignments}, app_state_updated_at = ? WHERE id = ?",
if course_ids:
placeholders = ",".join("?" * len(course_ids))
for row in conn.execute(
f"SELECT * FROM schoology_courses WHERE course_id IN ({placeholders})",
Comment on lines +426 to +427
f"SELECT * FROM schoology_assignments"
f" WHERE course_id IN ({placeholders}) AND due_at_ms >= ?",
if course_ids:
placeholders = ",".join("?" * len(course_ids))
for row in conn.execute(
f"SELECT * FROM schoology_courses WHERE course_id IN ({placeholders})",
Comment thread backend/db/chat_store.py
Comment on lines +975 to +976
except Exception:
pass
subprocess with a watcher thread.
"""
import logging
import subprocess
Comment on lines +116 to +117
process = subprocess.Popen(
[sys.executable, "-m", "services.chat.worker", generation_id],
assert verify_convex_token(mobile_token) is None
assert verify_mobile_access_token(convex_token) is None
other_token = create_token(**USER, audience=other_audience)
assert verify_token(mobile_token, audience=other_audience) is None
assert verify_mobile_access_token(convex_token) is None
other_token = create_token(**USER, audience=other_audience)
assert verify_token(mobile_token, audience=other_audience) is None
assert verify_mobile_access_token(other_token) is None
)
except Exception:
logger.exception(
"chat_launcher_mark_failed_error generation_id=%s", generation_id
)
except Exception:
logger.warning(
"chat_launcher_publish_terminal_error generation_id=%s", generation_id
"""
if not _mark_generation_started(generation_id):
logger.info(
"chat_generation_already_running generation_id=%s", generation_id
except Exception as exc:
_mark_generation_finished(generation_id)
message = str(exc) or exc.__class__.__name__
logger.exception("chat_generation_spawn_failed generation_id=%s", generation_id)
AdamEXu and others added 4 commits July 7, 2026 17:44
These files predate the ruff config #8 added, so CI's ruff gate flagged them
once the branch was merged with dev. Pure formatting — import sorting and one
placeholder-less f-string; no logic change (compile + full pytest still green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integrate TinyFish's free Search and Fetch APIs as chat tools so the
assistant can read external links (e.g. a course-catalog PDF that Schoology
only exposes as a link) and search the public web — capabilities the chat
did not have before.

- services/tinyfish.py: thin requests-based client for the Fetch
  (POST api.fetch.tinyfish.ai) and Search (GET api.search.tinyfish.ai)
  REST endpoints, authenticated with a single shared X-API-Key. No new
  dependency and no Python 3.11+ floor (avoids the tinyfish SDK).
- services/chat/web_tools.py: fetch_url and web_search tool definitions +
  execution, returning the existing ToolExecutionResult shape. Output is
  bounded (per-URL char cap, capped URL/result counts).
- services/chat/service.py: offer the web tools on every generation,
  independent of schoology_connected (fetching a public URL needs no
  Schoology auth), and dispatch web tool names to execute_web_tool.
- config.py: TINYFISH_API_KEY env var + a validate() notice when unset.
- prompts/chat_system.txt: tell the model about the new tools.

Set TINYFISH_API_KEY in backend/.env to enable. Key from
agent.tinyfish.ai/api-keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Web-tool error messages raised by the TinyFish client are streamed to the
chat UI as errorText, so "TinyFish fetch error: ..." etc. would surface to
end users. Reword the raised messages to generic "Web fetch"/"Web search"
wording and make the not-configured message user-friendly. Internal
identifiers (module, exception classes, env var, endpoints, comments) are
not user-facing and are left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add TinyFish web tools (fetch_url + web_search) to chat
@AdamEXu AdamEXu marked this pull request as ready for review July 8, 2026 07:20
@AdamEXu AdamEXu merged commit 50199dc into dev Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants