The 3-Layer Portability Model
Every ConvexCompose module is built in three layers. The key insight: each layer has different portability properties, and you only pay the framework-coupling cost at the very top.
Layer 1: Convex Core (100% portable)
The Convex schema and functions. Pure TypeScript with zero framework dependencies. This is where your data lives, your queries run, and your mutations execute.
- Drop the folder into any Convex project.
- No React, no Next.js, no TanStack imports.
- Tested with Convex's built-in test harness.
Layer 2: React Components (~90% portable)
Standard React components that consume the Convex hooks (useQuery, useMutation). They are framework-agnostic — no router imports, no next/link, no TanStack Link. Navigation happens through callback props that the host app wires up.
- Pass
onNavigatefrom the host's router. - Pass
renderLinkfor SSR-friendly links. - Style with Tailwind utility classes (or override).
Layer 3: Route Wrappers (framework-specific)
Thin shells — usually about 5 lines each — that adapt the React components to your specific router. ConvexCompose ships templates for TanStack Start, Next.js, and Remix.
// apps/web/src/routes/mail.tsx (TanStack Start)
import { createFileRoute } from '@tanstack/react-router';
import { MailInbox } from '@convexcompose/mail/react';
export const Route = createFileRoute('/mail')({
component: () => <MailInbox onOpen={(id) => Route.navigate({ to: `/mail/${id}` })} />,
}); Why this matters
If you swap routers (or even rewrite your frontend in a different framework entirely), you only rewrite Layer 3. Your data, your queries, and 90% of your UI come along for the ride.