Migrate entire backend to Convex (drop Prisma / Inngest / Redis / server routes) - #127
Conversation
…server routes) Complete migration of SaveIt onto a single Convex backend (packages/backend, @workspace/backend) as the source of truth for data, auth, jobs, AI, billing, email, files and the public API. No more Prisma / Postgres, Inngest, Upstash Redis, the Cloudflare worker, or TanStack server routes for backend logic. Backend (packages/backend/convex) — pushes clean to the dev deployment: - Better Auth on Convex via a LOCAL @convex-dev/better-auth component (user-centric, SaveIt custom user fields on the auth `user` table so userId === BA user id). Plugins: emailOTP (+ App Store test bypass), magicLink, admin, apiKey (@better-auth/api-key), expo, crossDomain, convex. Banned-user enforcement + typed ConvexError helpers (utils/errors). - Domain schema: bookmarks (single combined 1536-d searchEmbedding vector index), tags + bookmarkTags join, bookmarkOpens, processingRuns, chatConversations + chatMessages (child table), chatUsages, subscriptions, userCounters, changelogDismissals. - Bookmarks/tags CRUD + plan limits; processing pipeline (Inngest -> Convex actions + scheduler + reactive processingStep) with all 7 type handlers, Gemini summaries/embeddings, Cloudflare Browser Rendering screenshots, R2; vector search (pgvector -> ctx.vectorSearch); AI chat (HTTP streaming action); Stripe billing + webhook; Resend email + marketing drips + crons; public /api/v1 + extension + tools httpActions; admin/* (adminQuery). - All background jobs run via Convex crons + ctx.scheduler. sharp replaced by pure-JS image-size for serverless compatibility. Web (apps/web): full data-layer swap up-fetch/Prisma -> Convex (convexQuery / useConvexMutation / convexAction); ConvexBetterAuthProvider; SSR auth via convexBetterAuthReactStart. Deleted ~30 Prisma api.*.ts routes (kept only the api.auth.$ proxy that @convex-dev/better-auth requires). Removed @workspace/database, Prisma, inngest, @upstash/redis, up-fetch and server-only deps. `pnpm ts` + `pnpm lint` clean. Mobile (apps/mobile): wired to the shared Convex backend with the nowstack-mobile auth pattern (expoClient + SecureStore bearer tokens + crossDomain + convex). Typechecks clean. Extensions (chrome/firefox): session-cookie auth against Convex + new extension save/upload httpActions. Removed: packages/database (Prisma), apps/worker (folded into Convex), all Inngest/Redis/Prisma imports across every application.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34efd1730c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Check user counters for limit | ||
| const counters = await ctx.db | ||
| .query("userCounters") | ||
| .withIndex("by_user", (q) => q.eq("userId", args.userId)) | ||
| .first(); |
There was a problem hiding this comment.
Enforce limits before helper creates bookmarks
This helper is now used by both POST /api/v1/bookmarks and the browser-extension /api/bookmarks endpoint, but it only reads userCounters and never calls assertCanCreateBookmark before inserting. In those contexts a free user who has already reached the bookmark or monthly-run limit can keep creating bookmarks through the extension/API path, while the normal bookmarks.mutations.create path still enforces the limit.
Useful? React with 👍 / 👎.
| const result: { page: Array<Record<string, unknown>>; isDone: boolean; continueCursor?: string } = await ctx.runQuery( | ||
| internal.bookmarks.queries.listDefault, | ||
| { | ||
| userId: args.userId, | ||
| paginationOpts, |
There was a problem hiding this comment.
Route public text searches through the search action
When /api/v1/bookmarks?query=... is called without tags, this branch ignores args.query and runs listDefault, so SDK/API clients get an unfiltered bookmark list instead of search results. The previous route passed query to cachedAdvancedSearch, so any public API text search or public-slug search that relies on this helper regresses.
Useful? React with 👍 / 👎.
| const tagsList = p.tags | ||
| ? p.tags.split(",").map((t) => t.trim()).filter(Boolean) | ||
| : []; | ||
| const specialFilters: string[] = p.special ? [p.special.toLowerCase()] : []; |
There was a problem hiding this comment.
Preserve special filters in uppercase
For requests such as /api/v1/bookmarks?tags=work&special=STAR, this lowercases the value to star before forwarding it to searchByTags, whose validator only accepts READ, UNREAD, or STAR. That makes valid API requests with both tags and special fail validation instead of returning the filtered results.
Useful? React with 👍 / 👎.
…ktree dev scripts
- Drop @tanstack/react-query and @convex-dev/react-query; use useQuery from convex/react and a new use-async-task helper for mutations/actions - Simplify Providers to a single ConvexReactClient - Update backend convex functions, migration scripts, and plan docs - Fix landing header border to span full viewport width
…xport ordering - Prisma BookmarkTag has no userId column; resolve it via the bookmark's owner - Tag and subscription tables have no createdAt; order exports by id - Document CONVEX_DEPLOY_KEY handling in AGENTS.md
- vercel-build.mjs: when CONVEX_DEPLOY_KEY present, run `convex deploy --cmd-url-env-var-name VITE_CONVEX_URL --cmd 'node scripts/build-web.mjs'` so the deployed Convex URL is injected into the build automatically. - build-web.mjs: derive VITE_CONVEX_SITE_URL (.site) from VITE_CONVEX_URL. - migrate: idempotent imports (tags/bookmarks/bookmarkTags dedup) + withRetry on transient network errors so a crashed import resumes safely. - mobile: bump to 3.0.0, add EXPO_PUBLIC_CONVEX_URL/SITE_URL + expo-network.
Lets the branch preview point at an existing deployment (e.g. dev with migrated data) instead of deploying functions to a fresh empty preview deployment. Production (no manual VITE_CONVEX_URL) still auto-deploys via the deploy key.
- importConversations: idempotent (dedup by user+updatedAt+title) and skip individual chat messages over the 1 MiB Convex doc limit. - rebuildUserCounters: take precomputed per-user counts instead of paginating inside the mutation (Convex forbids multiple paginated queries / blows read limits for power users); counts are computed client-side from the export. - import-convex.ts: ONLY_STAGES gating + per-row conversation batches (CONVERSATION_CHUNK_SIZE) so a failed stage can be re-run in isolation; summary uses hoisted counters so skipped stages don't throw.
Replace the monolithic pipeline action with a journaled workflow: named retryable steps (limits, dedupe, route, type handler, finish), onComplete error/cancel handling with run-ownership guard, workflowId stored on bookmarks so deletion cancels in-flight runs. Note: pnpm-lock.yaml also picks up regeneration from earlier uncommitted dependency changes on this branch.
Browse mode now subscribes via usePaginatedQuery so new PENDING bookmarks and status flips stream in without refetch plumbing; the pending card animates processingStep again (parity with the old Inngest realtime UI). Search action: tags-only requests reach searchByTags (Route 1 swallowed them); unreachable Route 2 removed.
Better Auth resolves the client session before ConvexBetterAuthProvider installs the JWT; authQueries fired in that window threw UNAUTHORIZED into the error boundary and crashed /app. New useAuthedQuery hook skips until useConvexAuth is ready; all 16 authed call sites migrated. Also: error card reads processingError (now in BookmarkDTO) instead of crashing on metadata.error when metadata is absent.
URLs pasted from prose (e.g. "…/page:") 404ed on fetch and produced empty READY bookmarks. cleanUrl now strips trailing punctuation and unbalanced closing brackets (Wikipedia-style "_(theory)" preserved).
…ence) Thumbfast-style rule: dev-browser sign-in via OTP read from the Better Auth verification table, plus processing-evidence checks. AGENTS.md and CLAUDE.md also pick up the branch's pending Prisma-to-Convex doc updates.
convex codegen requires CONVEX_DEPLOYMENT, which CI doesn't have. Run convex dev --once in anonymous local mode instead, then diff convex/_generated to keep the consistency check without secrets.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Consolidate the DNS-resolving SSRF guard (previously local to files/actions.ts) into a shared lib/safe_fetch.ts and route the bookmark-processing fetches (steps analyzeUrl/fetchHtml, image + PDF downloads, storage.uploadFromURL) through it. Close the DNS gap on the public V8-runtime tool endpoints via a Node internalAction (tools/node_fetch.ts) called from fetchHtml/validateFavicon.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
The chat HTTP action only validated Better Auth session cookies, which never reach convex.site cross-origin, so every /chat request returned 401 and the agent never replied. Resolve the user from the Convex JWT (safeGetAuthUser) first, keeping the cookie session as fallback.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…re compliance - Native Sign in with Apple (expo-apple-authentication -> Better Auth idToken), iOS entitlement + plugin - RevenueCat paywall (purchase/restore, backend-confirmed activation), iOS upgrade path off web checkout - Convex /revenuecat/webhook with shared secret, replay guard, Stripe-ownership protection, TRANSFER revocation - subscriptions schema: provider, revenuecatProductId, revenuecatLastEventAt - Apple client secret generator script (Guidelines 4.8 + 3.1.1)
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
- expo-iap (StoreKit 2) purchase/restore on mobile, no third-party SDK - Convex verifies via App Store Server API (ES256 JWT, prod->sandbox fallback) - App Store Server Notifications V2 endpoint: trigger-only, re-verifies with Apple - subscriptions: provider appstore + originalTransactionId index, ownership guard
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…aunch recovery for unsynced transactions, app-scoped IAP connection
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…iled delivery); add ASC submit key to eas.json
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…d-in ios/ overrides app.json versions
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
Summary
Complete migration of SaveIt onto a single Convex backend (
packages/backend,@workspace/backend) as the source of truth for data, auth, jobs, AI, billing, email, files and the public API.Gone entirely: Prisma/Postgres (
@workspace/database), Inngest, Upstash Redis, the Cloudflare worker (apps/worker), and all TanStack server routes that held backend logic. Verified: zero@prisma/@workspace/database/inngest/@upstash/redis/nextimports across every application.Backend (
packages/backend/convex) — deploys clean ✅@convex-dev/better-authcomponent (user-centric; SaveIt custom user fields on the authusertable →userId === BA user id). Plugins: emailOTP (+ App Store test bypass), magicLink, admin, apiKey, expo, crossDomain, convex. Banned-user enforcement + typedConvexErrorhelpers.searchEmbeddingvector index), tags +bookmarkTags,bookmarkOpens, processing runs,chatConversations+chatMessages(child table),chatUsages, subscriptions,userCounters,changelogDismissals.ctx.scheduler+ reactiveprocessingStep, all 7 type handlers, Gemini, Cloudflare Browser Rendering, R2) · vector search (pgvector →ctx.vectorSearch) · AI chat (HTTP streaming action) · Stripe billing + webhook · Resend email + marketing drips + crons · public/api/v1+ extension + tools httpActions ·admin/*(adminQuery/Mutation/Action).ctx.scheduler.sharp→ pure-JSimage-sizefor serverless.Web (
apps/web) ✅pnpm ts+pnpm lintcleanFull data-layer swap
up-fetch/Prisma → Convex (convexQuery/useConvexMutation/convexAction).ConvexBetterAuthProvider+ SSR auth viaconvexBetterAuthReactStart. Deleted ~30 Prismaapi.*.tsroutes (kept only theapi.auth.$proxy that@convex-dev/better-authrequires). Removed all server-only deps +db:*turbo tasks.Mobile (
apps/mobile) ✅ typechecks cleanWired to the shared Convex backend with the nowstack-mobile auth pattern (
expoClient+ SecureStore bearer tokens + crossDomain + convex).Extensions (chrome/firefox)
Session-cookie auth against Convex + new extension save/upload httpActions.
Notes / follow-ups
createServerFnusages inapps/webare Convex-only SSR loaders (they callfetchAuthQuery→ Convex, no backend logic). Can be converted to pure clientuseQueryif literally-zero server functions is required.npx convex env set(Stripe/Gemini/R2/Resend/OAuth) before production cutover..agents/plan/convex-refactor/.🤖 Generated with Claude Code