Getting started
This walks the three steps a consumer takes to use any ConvexCompose module: install the Component with app.use(), call its headless hook, and bind it into your router with a roughly five-line wrapper. The example is convex-garden, the first module in extraction and the worked example throughout the docs.
Honest status: no module is a published, installable package yet. The steps below are the real consumption shape; the package names are the intended ones. convex-garden is mid-extraction from a real production deployment into a conforming Component.
1. Prerequisites
- An existing Convex project (
npx create-convex@latest) - Node 20+ or Bun 1.x
- A canonical user identity in your app (for example a Better Auth
userId) to pass into modules
2. Install the Component (Layer 1)
A module's Layer 1 is a Convex Component. Install it into your deployment with app.use(), exactly like any first-wave Convex Component such as the rate limiter or aggregate.
// convex/convex.config.ts
import { defineApp } from "convex/server";
import garden from "convex-garden/convex.config";
const app = defineApp();
app.use(garden);
export default app; 3. Call the headless hook (Layer 2)
A module's Layer 2 is a router-free, style-free useX() hook. Navigation is a callback you pass in; the hook imports no router and assumes no styling. Note the canonical userId is passed in explicitly (the cross-module linking rule); authorization still happens from ctx inside the Component's handlers.
// the module exposes this; you just call it
const { plants, addPlant, removePlant, openPlant } = useGarden(userId, {
onPlantOpened: (id) => goToPlant(id), // navigation is YOUR callback
}); 4. Write the route wrapper (Layer 3)
Layer 3 is the only layer you write, and it is intentionally trivial. It binds the hook into your actual router and chrome. The router lives here, never in the module. The same five lines work against any framework; here is TanStack Start, then Next.
// TanStack Start: app/routes/garden.tsx
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useGarden } from "convex-garden/react";
export const Route = createFileRoute("/garden")({
component: () => {
const navigate = useNavigate();
const garden = useGarden(userId, { onPlantOpened: (id) => navigate({ to: `/garden/${id}` }) });
return <YourGardenUI {...garden} />; // your UI, or the optional reference kit
},
}); // Next: app/garden/page.tsx
"use client";
import { useRouter } from "next/navigation";
import { useGarden } from "convex-garden/react";
export default function GardenPage() {
const router = useRouter();
const garden = useGarden(userId, { onPlantOpened: (id) => router.push(`/garden/${id}`) });
return <YourGardenUI {...garden} />;
} 5. Optional: drop in the reference UI
If you want batteries included instead of writing your own UI, the module ships an OPTIONAL reference UI package (React Native plus NativeWind). It is a drop-in Layer 3 surface; ignore it and you lose nothing but the convenience. A web-only consumer never has to touch React Native to use the Component and hooks.
6. Compose with other modules
Modules coexist because cross-module relationships are expressed by passing canonical IDs in, never by cross-Component joins. Pass the same canonical userId into Garden, Mail, and Tasks, and they link by construction while staying isolated and transactional.
Want the full rules behind these steps? Read the contract. Building a module? Run the checklist.