@workspace/convex
Pre-configured Convex React client with null-safe CI pattern
Singleton Convex client that gracefully handles missing configuration in CI environments. Returns null
when NEXT_PUBLIC_CONVEX_URL
is unset or points to placeholder URLs.
Package Exports
Core Export
convexClient
Pre-configured ConvexReactClient
or null
when Convex is unavailable.
import { convexClient } from "@workspace/convex";
// convexClient: ConvexReactClient | null
Initialization Logic
export const convexClient =
publicEnv.NEXT_PUBLIC_CONVEX_URL &&
publicEnv.NEXT_PUBLIC_CONVEX_URL !== "https://placeholder.convex.cloud"
? new ConvexReactClient(publicEnv.NEXT_PUBLIC_CONVEX_URL)
: null;
Check if NEXT_PUBLIC_CONVEX_URL
is defined
Verify URL is not the placeholder value
If both pass, create ConvexReactClient
instance
Otherwise, return null
ConvexClient Type
import type { ConvexClient } from "@workspace/convex";
// Type of convexClient (ConvexReactClient | null)
Usage Patterns
Provider Setup
Wrap your application when convexClient
exists:
import { ConvexProvider } from "convex/react";
import { convexClient } from "@workspace/convex";
export function Providers({ children }: { children: React.ReactNode }) {
if (!convexClient) {
// CI mode or missing config - render without Convex
return <>{children}</>;
}
return (
<ConvexProvider client={convexClient}>
{children}
</ConvexProvider>
);
}
Heads up
Always check for null before using convexClient
in provider setup. Null indicates CI environment or missing configuration.
Query Usage
"use client";
import { useQuery } from "convex/react";
import { api } from "@convex/_generated/api";
import { convexClient } from "@workspace/convex";
export function DocumentList() {
const documents = useQuery(api.documents.list);
if (!convexClient) {
return <div>Real-time features unavailable</div>;
}
if (documents === undefined) {
return <div>Loading...</div>;
}
return (
<ul>
{documents.map(doc => (
<li key={doc._id}>{doc.title}</li>
))}
</ul>
);
}
When wrapped in ConvexProvider
, components can use hooks directly:
"use client";
import { useQuery } from "convex/react";
import { api } from "@convex/_generated/api";
export function UserProfile() {
const profile = useQuery(api.users.getCurrentProfile);
if (profile === undefined) return <div>Loading...</div>;
if (profile === null) return <div>Not found</div>;
return <div>{profile.name}</div>;
}
Convex queries return undefined
while loading:
const data = useQuery(api.example.get);
// undefined: query loading or Convex unavailable
if (data === undefined) {
return <Skeleton />;
}
// null: query completed, no data found
if (data === null) {
return <EmptyState />;
}
// data exists
return <DisplayData data={data} />;
Mutation Usage
"use client";
import { useMutation } from "convex/react";
import { api } from "@convex/_generated/api";
export function CreateDocument() {
const create = useMutation(api.documents.create);
const handleCreate = async () => {
try {
const id = await create({ title: "New Document" });
console.log("Created:", id);
} catch (error) {
console.error("Creation failed:", error);
}
};
return <button onClick={handleCreate}>Create Document</button>;
}
Mutation Error Handling
Mutations throw errors when they fail. Always wrap mutation calls in try-catch blocks or use error boundaries.
Architecture
Null Client Pattern
The null-safe pattern solves three problems:
CI Builds Without Backend
Next.js builds succeed in CI without provisioning Convex deployments. Pages render in loading states during build, hydrate with real data in browsers.
Placeholder Detection
Detects placeholder URLs like https://placeholder.convex.cloud
and treats them as missing config, preventing connection errors in dev environments.
Graceful Degradation
Applications render without Convex, showing appropriate loading or unavailable states. No crashes from missing WebSocket connections.
Singleton Instance
One shared client instance prevents multiple WebSocket connections:
// ✅ Single import, single connection
import { convexClient } from "@workspace/convex";
// ❌ Don't create new clients
const client = new ConvexReactClient(url); // Multiple connections!
Multiple clients cause:
- Memory leaks from duplicate connections
- Increased latency from connection overhead
- Subscription deduplication failures
Environment Detection
Valid Convex URLs must:
- Be defined (not
undefined
or empty string) - Not match placeholder values
- Point to actual
*.convex.cloud
or*.convex.site
domains
// Valid URLs
"https://happy-animal-123.convex.cloud"
"https://my-deployment.convex.site"
// Invalid URLs (trigger null client)
"https://placeholder.convex.cloud"
""
undefined
CI/CD Integration
Build Behavior
During next build
:
NEXT_PUBLIC_CONVEX_URL
is read from environment
If missing or placeholder, convexClient
is null
Pages render without Convex data (loading states)
Build completes successfully
In browser, client hydrates and establishes connection
Test Configuration
# Option 1: Use placeholder (null client)
NEXT_PUBLIC_CONVEX_URL=https://placeholder.convex.cloud
# Option 2: Omit entirely (null client)
# NEXT_PUBLIC_CONVEX_URL=
# Option 3: Use real deployment (requires Convex access)
NEXT_PUBLIC_CONVEX_URL=https://test-deployment.convex.cloud
Mock Convex in tests:
import { vi } from "vitest";
vi.mock("convex/react", () => ({
useQuery: vi.fn(() => [{ id: "1", title: "Test" }]),
useMutation: vi.fn(() => vi.fn()),
ConvexProvider: ({ children }: any) => children,
}));
Performance Considerations
Connection Management
The singleton pattern optimizes connection usage:
- One WebSocket serves all subscriptions
- Automatic reconnection on disconnect
- Query deduplication across components
- Efficient subscription batching
Bundle Size
The Convex client adds approximately:
convex/react
: ~50KB gzipped@convex-dev/better-auth
: ~10KB gzipped
Tree-shaking eliminates unused functionality.
Hot Reload
During development, the client maintains connections across hot reloads. Subscriptions persist without reconnection overhead.
Error Handling
Connection Failures
When Convex is configured but unreachable:
const data = useQuery(api.example.get);
// undefined while connecting or connection failed
if (data === undefined) {
return <div>Loading...</div>;
}
// Connection restored, data available
return <DisplayData data={data} />;
No Explicit Error State
Convex queries return undefined
during connection issues. They don't expose explicit error objects. Use undefined state for loading indicators.
Invalid Configuration
Malformed URLs cause initialization failures:
[Convex] Failed to connect: Invalid URL
Production builds treat invalid config as missing config, returning null clients without throwing errors.
Security Integration
Authentication Bridge
Convex integrates with BetterAuth through ConvexBetterAuthProvider
:
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/client";
import { authClient } from "@workspace/auth/client";
import { convexClient } from "@workspace/convex";
export function Providers({ children }: { children: React.ReactNode }) {
if (!convexClient) return <>{children}</>;
return (
<ConvexBetterAuthProvider client={authClient}>
<ConvexProvider client={convexClient}>
{children}
</ConvexProvider>
</ConvexBetterAuthProvider>
);
}
Authentication state synchronizes automatically. Convex queries receive authenticated context.
Query Visibility
All data returned by Convex queries is visible to clients. Security depends on server-side query implementations in convex/
directory, not client-side filtering.
// ❌ Client-side filtering doesn't protect data
const data = useQuery(api.admin.getAllUsers);
const visible = data?.filter(u => u.id === currentUserId);
// ✅ Server-side filtering in Convex function
export const getMyData = query({
handler: async (ctx) => {
const user = await ctx.auth.getUser();
return ctx.db.query("data")
.filter(q => q.eq(q.field("userId"), user.userId))
.collect();
}
});
Debugging
Connection Status
Check connection in browser console:
import { convexClient } from "@workspace/convex";
if (convexClient) {
console.log("Convex URL:", convexClient.connectionState);
}
Query Inspection
const data = useQuery(api.example.get, { arg: "value" });
useEffect(() => {
console.log("Query result:", data);
console.log("Is loading:", data === undefined);
}, [data]);
React DevTools
The Convex provider appears in React DevTools component tree. Inspect provider props to verify client configuration.
The convex package provides a pre-configured client that handles the complexity of WebSocket management, authentication integration, and CI compatibility. For custom Convex configuration, extend the client setup in app/providers.tsx
rather than creating new client instances.