diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..e5b6d8d6 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..0233baeb --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": [ + "@changesets/changelog-github", + { "repo": "udecode/better-convex" } + ], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": ["example"] +} diff --git a/.changeset/first-release.md b/.changeset/first-release.md new file mode 100644 index 00000000..4c3d8988 --- /dev/null +++ b/.changeset/first-release.md @@ -0,0 +1,5 @@ +--- +"better-convex": minor +--- + +Initial release diff --git a/.claude/AGENTS.md b/.claude/AGENTS.md index f04fdd90..60ae3ef7 100644 --- a/.claude/AGENTS.md +++ b/.claude/AGENTS.md @@ -1,4 +1,7 @@ -- In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. +- In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision +- ALWAYS read and understand relevant files before proposing edits. Do not speculate about code you have not inspected +- ALWAYS use AskUserQuestion tool when asking questions to the user +- When playwriter browser requires start dev, auth or disconnects, use AskUserQuestion: (1) Connected (2) Skip browser test. Never close browser when done. ## PR Comments diff --git a/.claude/docs/ERRORS.md b/.claude/docs/ERRORS.md new file mode 100644 index 00000000..e69de29b diff --git a/.claude/docs/plan.md b/.claude/docs/plan.md new file mode 100644 index 00000000..a03b6087 --- /dev/null +++ b/.claude/docs/plan.md @@ -0,0 +1,88 @@ +# Better-Convex Documentation Test + +## Objective + +Test the better-convex documentation by building a working Next.js 15 app from scratch. Simulate a new developer with no prior better-convex knowledge. + +## Rules + +### Knowledge Boundaries + +1. **Only use `better-convex/content/docs/`** as reference +2. **Convex native knowledge allowed** (official Convex patterns) +3. **When stuck, document the gap** - don't guess +4. Do not read anything outside `better-convex` folder. + +### Error Tracking + +Create `ERRORS.md` and log every issue: + +```md +### Error N: [Title] + +- **Location**: Doc page, step +- **Expected**: What docs said +- **Actual**: What happened +- **Resolution**: Workaround used +- **Doc Fix**: Suggested improvement +``` + +Log: missing steps, wrong code, missing imports, type errors, runtime errors, missing deps, broken links + +### Process + +1. Read docs first, then act +2. Follow docs literally - copy code exactly +3. Log confusion before proceeding +4. No silent fixes - log every doc gap + +## Test Scope + +Build a complete app following the docs. Exclude auth (separate test). + +## Combination Strategy + +Test one combo thoroughly first, then expand. Based on index.mdx "For AI Agents" options. + +### Combo 1 (Current): Default Stack + +| Choice | Value | +|--------|-------| +| Approach | Top-down (Templates) | +| Framework | Next.js App Router | +| Database | ctx.table (Ents) | +| Auth | None (excluded) | +| SSR/RSC | Yes | +| Triggers | Yes | + +### Future Combos (after Combo 1 passes) + +| # | Approach | Framework | DB | Auth | Notes | +|---|----------|-----------|-----|------|-------| +| 2 | Top-down | Next.js | ctx.db | None | Vanilla DB | +| 3 | Bottom-up | Vite | ctx.table | None | Non-Next.js | +| 4 | Bottom-up | Next.js | ctx.table | Better Auth | Auth test | +| 5 | Top-down | Vite | ctx.db | None | Minimal stack | + +## Verification + +- App runs +- Features work +- Real-time updates + +## Success Criteria + +### Combo 1 +- [x] TypeScript passes +- [ ] App runs without errors +- [ ] Real-time updates work +- [ ] ERRORS.md documents all gaps + +### Overall +- [ ] All combos tested +- [ ] Doc fixes applied + +## Output + +1. `ERRORS.md` - Issues found +2. Summary - Doc quality assessment diff --git a/.claude/docs/references/better-auth/authorization.mdx b/.claude/docs/references/better-auth/authorization.mdx new file mode 100644 index 00000000..cdb6a3ab --- /dev/null +++ b/.claude/docs/references/better-auth/authorization.mdx @@ -0,0 +1,92 @@ +--- +title: Authorization +description: Authorization with Better Auth +--- + +### Showing UI based on authentication state + +You can control which UI is shown when the user is signed in or signed out using +Convex's ``, `` and `` helper +components. These components are powered by Convex's `useConvexAuth()` hook, +which provides `isAuthenticated` and `isLoading` flags. This hook can be used +directly if preferred. + +It's important to use Convex's authentication state components or the +`useConvexAuth()` hook instead of Better Auth's `getSession()` or `useSession()` +when you need to check whether the user is logged in or not. Better Auth will +reflect an authenticated user before Convex does, as the Convex client must +subsequently validate the token provided by Better Auth. Convex functions that +require authentication can throw if called before Convex has validated the +token. + +In the following example, the `` component is a child of +``, so its content and any of its child components are guaranteed +to have an authenticated user, and Convex queries can require authentication. + +```tsx title="src/App.tsx" +import { + Authenticated, + Unauthenticated, + AuthLoading, + useQuery, +} from "convex/react"; +import { api } from "../convex/_generated/api"; + +function App() { + return ( +
+ Logged out + Logged in + Loading... +
+ ); +} + +const Content = () => { + const messages = useQuery(api.messages.getForCurrentUser); + return
Authenticated content: {messages?.length}
; +}; + +export default App; +``` + +### Authentication state in Convex functions + +If the client is authenticated, you can access the information stored in the JWT +via `ctx.auth.getUserIdentity`. + +If the client is **not** authenticated, `ctx.auth.getUserIdentity` will return +null. + +Make sure that the component calling this query is a child of `` +from `convex/react`, or that `isAuthenticated` from `useConvexAuth()` is `true`. +Otherwise, it will throw on page load. + +```ts title="convex/messages.ts" +import { query } from "./_generated/server"; + +// You can get the current user from the auth component with session validation. +export const getCurrentUser = query({ + args: {}, + handler: async (ctx) => { + return await authComponent.getAuthUser(ctx); + }, +}); + +// You can also just get the authenticated user id as you +// normally would from ctx.auth.getUserIdentity. Note that +// this does not validate the session. +export const getForCurrentUser = query({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (identity === null) { + throw new Error("Not authenticated"); + } + return await ctx.db + .query("messages") + .filter((q) => q.eq(q.field("author"), identity.email)) + .collect(); + }, +}); +``` diff --git a/.claude/docs/references/better-auth/basic-usage.mdx b/.claude/docs/references/better-auth/basic-usage.mdx new file mode 100644 index 00000000..7f34e0fe --- /dev/null +++ b/.claude/docs/references/better-auth/basic-usage.mdx @@ -0,0 +1,99 @@ +--- +title: Basic Usage +description: Using Better Auth with Convex +--- + +## Better Auth guide + +Better Auth's [basic usage guide](https://www.better-auth.com/docs/basic-usage) +applies to Convex as well. It covers signing in and out, social providers, +plugins, and more. You will be using Better Auth directly in your project, so +their guides are a primary reference. + +### Exceptions + +There are a few areas in the Better Auth basic usage guide that work differently +in Convex. + +- #### Server side authentication + + Better Auth supports signing users in and out through server side functions. + Because Convex functions run over websockets and don't return HTTP responses + or set cookies, signing up/in/out must be done from the client via + `authClient.signIn.*` methods. + +- #### Schemas and migrations + + The basic usage guide includes information on database schema generation and + migrations via the Better Auth CLI. This only applies for + [local installs](/local-install), which support generating schemas. For + projects not using local install, the default schema provided with the Better + Auth component (preconfigured with the + [supported plugins](/supported-plugins)) is used, and cannot be altered. + +## Using server methods with `auth.api` + +Better Auth's server side `auth.api` methods can be used with your `createAuth` +function and the component `headers` method. Here's an example implementing the +[`changePassword` server method](https://www.better-auth.com/docs/concepts/users-accounts#api-method-change-password). + +```ts +export const updateUserPassword = mutation({ + args: { + currentPassword: v.string(), + newPassword: v.string(), + }, + handler: async (ctx, args) => { + // Many Better Auth server methods require a currently authenticated + // user, so request headers have to be passed in so session cookies + // can be parsed and validated. The `getAuth` method provides both the + // auth object and headers for convenience. + const { auth, headers } = await authComponent.getAuth(createAuth, ctx); + await auth.api.changePassword({ + body: { + currentPassword: args.currentPassword, + newPassword: args.newPassword, + }, + headers, + }); + }, +}); +``` + +## Using Convex ctx in Better Auth config + +The `ctx` param passed in to the `createAuth` function is the Convex context +object. This can be used to access the Convex database or Convex functions in +your Better Auth config. It can be a +[query](https://docs.convex.dev/functions/query-functions#query-context), +[mutation](https://docs.convex.dev/functions/mutation-functions#mutation-context), +or [action](https://docs.convex.dev/functions/actions#action-context) context. + +A common use case is sending emails for verification or password resets with the +[Resend component](https://www.convex.dev/components/resend). `resend.sendEmail` +will produce a type error because the ctx object could be a query ctx. The +component provides type guards for this. + +```ts +import { requireActionCtx } from "@convex-dev/better-auth/utils"; +import { type GenericCtx } from "@convex-dev/better-auth"; +import { Resend } from "@convex-dev/resend"; +import { components } from "./_generated/api"; +import { type DataModel } from "./_generated/dataModel"; + +export const resend = new Resend(components.resend); + +export const createAuthOptions = (ctx: GenericCtx) => ({ + baseURL: siteUrl, + sendVerificationEmail: async ({ user, url }) => { + // This function only requires a `runMutation` property on the ctx object, + // but we'll make sure we have an action ctx because we know a network + // request is being made, which requires an action ctx. + await resend.sendEmail(requireActionCtx(ctx), { + to: user.email, + subject: "Verify your email", + html: `

Click here to verify your email

`, + }); + }, +}); +``` diff --git a/.claude/docs/references/better-auth/better-convex-auth.md b/.claude/docs/references/better-auth/better-convex-auth.md new file mode 100644 index 00000000..00ff92bd --- /dev/null +++ b/.claude/docs/references/better-auth/better-convex-auth.md @@ -0,0 +1,409 @@ +# Better Auth Convex + +Local installation of Better Auth directly in your Convex app schema, with direct database access instead of component-based queries. + +## Why Better Auth Convex? + +The official `@convex-dev/better-auth` component stores auth tables in a component schema. This package provides an alternative approach with direct schema integration. + +**This package provides direct local installation:** + +1. **Auth tables live in your app schema** - Not in a component boundary +2. **Direct database access** - No `ctx.runQuery`/`ctx.runMutation` overhead (>50ms latency that increases with app size) +3. **Unified context** - Auth triggers can directly access and modify your app tables transactionally +4. **Full TypeScript inference** - Single schema, single source of truth + +> [!WARNING] +> BREAKING CHANGE: Auth tables are stored in your app schema instead of the component schema. If you're already in production with `@convex-dev/better-auth`, you'll need to write a migration script to move your auth data. + +## Prerequisites + +- Follow the [official Better Auth + Convex setup guide](https://labs.convex.dev/better-auth) first +- Choose your [framework guide](https://labs.convex.dev/better-auth/framework-guides/next) + - **IGNORE these steps from the framework guide**: + - Step 2: "Register the component" - We don't use the component approach + - Step 5: `convex/functions/auth.ts` - We'll use a different setup + - Step 8: `convex/http.ts` - We use different route registration +- Then come back here to install locally + +## Installation + +```bash +pnpm add better-auth@1.4.9 better-auth-convex +``` + +## Local Setup + +You'll need `convex/auth.config.ts` and update your files to install Better Auth directly in your app: + +```ts +// convex/auth.config.ts +import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config"; +import type { AuthConfig } from "convex/server"; + +export default { + providers: [getAuthConfigProvider({ jwks: process.env.JWKS })], +} satisfies AuthConfig; +``` + +```ts +// convex/functions/auth.ts +import { betterAuth, type BetterAuthOptions } from "better-auth"; +import { convex } from "@convex-dev/better-auth/plugins"; +import { admin, organization } from "better-auth/plugins"; // Optional plugins +import { + type AuthFunctions, + createClient, + createApi, +} from "better-auth-convex"; +import { internal } from "./_generated/api"; +import type { MutationCtx, QueryCtx, GenericCtx } from "./_generated/server"; +import type { DataModel } from "./_generated/dataModel"; +import schema from "./schema"; // YOUR app schema with auth tables +import authConfig from "./auth.config"; + +// 1. Internal API functions for auth operations +const authFunctions: AuthFunctions = internal.auth; + +// 2. Auth client with triggers that run in your app context +export const authClient = createClient({ + authFunctions, + schema, + triggers: { + user: { + beforeCreate: async (_ctx, data) => { + // Ensure every user has a username, filling in a simple fallback + const username = + data.username?.trim() || + data.email?.split("@")[0] || + `user-${Date.now()}`; + + return { + ...data, + username, + }; + }, + onCreate: async (ctx, user) => { + // Direct access to your database + // Example: Create personal organization + const orgId = await ctx.db.insert("organization", { + name: `${user.name}'s Workspace`, + slug: `personal-${user._id}`, + // ... other fields + }); + + // Update user with personalOrganizationId + await ctx.db.patch(user._id, { + personalOrganizationId: orgId, + }); + }, + beforeDelete: async (ctx, user) => { + // Example: clean up custom tables before removing the user + if (user.personalOrganizationId) { + await ctx.db.delete(user.personalOrganizationId); + } + + return user; + }, + }, + session: { + onCreate: async (ctx, session) => { + // Set default active organization on session creation + if (!session.activeOrganizationId) { + const user = await ctx.db.get(session.userId); + + if (user?.personalOrganizationId) { + await ctx.db.patch(session._id, { + activeOrganizationId: user.personalOrganizationId, + }); + } + } + }, + }, + }, +}); + +// 3. Auth options factory +export const createAuthOptions = (ctx: GenericCtx) => + ({ + baseURL: process.env.SITE_URL!, + plugins: [ + convex({ + authConfig, + jwks: process.env.JWKS, + }), + admin(), + organization({ + // Organization plugin config + }), + ], + session: { + expiresIn: 60 * 60 * 24 * 30, // 30 days + updateAge: 60 * 60 * 24 * 15, // 15 days + }, + database: authClient.httpAdapter(ctx), + // ... other config (social providers, user fields, etc.) + }) satisfies BetterAuthOptions; + +// 4. Create auth instance +export const createAuth = (ctx: GenericCtx) => + betterAuth(createAuthOptions(ctx)); + +// 5. IMPORTANT: Use getAuth for queries/mutations (direct DB access) +export const getAuth = (ctx: Ctx) => { + return betterAuth({ + ...createAuthOptions({} as any), + database: authClient.adapter(ctx, createAuthOptions), + }); +}; + +// 6. Export trigger handlers for Convex +export const { + beforeCreate, + beforeDelete, + beforeUpdate, + onCreate, + onDelete, + onUpdate, +} = authClient.triggersApi(); + +// 7. Export API functions for internal use +export const { + create, + deleteMany, + deleteOne, + findMany, + findOne, + updateMany, + updateOne, + getLatestJwks, + rotateKeys, +} = createApi(schema, createAuth, { + // Optional: Skip input validation for smaller generated types + // Since these are internal functions, validation is optional + skipValidation: true, +}); + +// Optional: If you need custom mutation builders (e.g., for custom context) +// Pass internalMutation to both createClient and createApi +// export const authClient = createClient({ +// authFunctions, +// schema, +// internalMutation: myCustomInternalMutation, +// triggers: { ... } +// }); +// +// export const { create, ... } = createApi(schema, createAuth, { +// internalMutation: myCustomInternalMutation, +// }); +``` + +The trigger API exposes both `before*` and `on*` hooks. The `before` variants run inside the same Convex transaction just ahead of the database write, letting you normalize input, enforce invariants, or perform cleanup and return any transformed payload that should be persisted. + +```ts +// convex/http.ts +import { httpRouter } from "convex/server"; +import { registerRoutes } from "better-auth-convex"; +import { createAuth } from "./auth"; + +const http = httpRouter(); + +registerRoutes(http, createAuth); + +export default http; +``` + +### Generate JWKS + +After deploying, generate and set the JWKS env var: + +```bash +# Development +npx convex run auth:getLatestJwks | npx convex env set JWKS + +# Production +npx convex run auth:getLatestJwks --prod | npx convex env set JWKS --prod +``` + +To rotate keys (invalidates all existing tokens, forces re-authentication): + +```bash +npx convex run auth:rotateKeys | npx convex env set JWKS +``` + +## Key Concepts + +### Direct DB Access vs HTTP Adapter + +```ts +// ✅ In queries/mutations: Use getAuth (direct DB access) +export const someQuery = query({ + handler: async (ctx) => { + const auth = getAuth(ctx); // Direct DB access + const user = await auth.api.getUser({ userId }); + }, +}); + +// ⚠️ In actions: Use createAuth (needs HTTP adapter for external calls) +export const someAction = action({ + handler: async (ctx) => { + const auth = createAuth(ctx); // Actions can't directly access DB + // Use for webhooks, external API calls, etc. + }, +}); +``` + +### Unified Schema Benefits + +```ts +// Component approach (@convex-dev/better-auth): +// - Auth tables in components.betterAuth schema +// - Requires ctx.runQuery/runMutation for auth operations +// - Component boundaries between auth and app tables + +// Local approach (better-auth-convex): +// ✅ Auth tables in your app schema +// ✅ Direct queries across auth + app tables +// ✅ Single transaction for complex operations +// ✅ Direct function calls +``` + +### Helper Functions + +All helpers are exported from the main package: + +```ts +import { getAuthUserId, getSession, getHeaders } from "better-auth-convex"; + +// Get current user ID +const userId = await getAuthUserId(ctx); + +// Get full session +const session = await getSession(ctx); + +// Get headers for auth.api calls +const headers = await getHeaders(ctx); +``` + +## API Options + +### `skipValidation` + +The `createApi` function accepts a `skipValidation` option that uses generic validators instead of typed validators: + +```ts +export const { create, ... } = createApi(schema, createAuth, { + skipValidation: true, // Smaller generated types +}); +``` + +**When to use**: Enable this option to significantly reduce generated type sizes. Since these are internal functions only called by the auth adapter, input validation is optional. The trade-off is less precise TypeScript inference for the internal API arguments. + +## Custom Mutation Builders + +Both `createClient` and `createApi` accept an optional `internalMutation` parameter, allowing you to wrap internal mutations with custom context or behavior. + +### Use Cases + +This is useful when you need to: + +- Wrap database operations with custom context (e.g., triggers, logging) +- Apply middleware to all auth mutations +- Inject dependencies or configuration + +### Example with Triggers + +```ts +import { customMutation, customCtx } from 'convex-helpers/server/customFunctions'; +import { internalMutationGeneric } from 'convex/server'; +import { registerTriggers } from '@convex/triggers'; + +const triggers = registerTriggers(); + +// Wrap mutations to include trigger-wrapped database +const internalMutation = customMutation( + internalMutationGeneric, + customCtx(async (ctx) => ({ + db: triggers.wrapDB(ctx).db, + })) +); + +// Pass to createClient +export const authClient = createClient({ + authFunctions, + schema, + internalMutation, // Use custom mutation builder + triggers: { ... } +}); + +// Pass to createApi +export const { create, updateOne, ... } = createApi(schema, createAuth, { + internalMutation, // Use same custom mutation builder +}); +``` + +This ensures all auth operations (CRUD + triggers) use your wrapped database context. + +## Updating the Schema + +Better Auth configuration changes may require schema updates. The Better Auth docs will often note when this is the case. To regenerate the schema (it's generally safe to do), run: + +```bash +cd convex && npx @better-auth/cli generate -y --output authSchema.ts +``` + +### Import Generated Schema (Recommended) + +Import the generated schema in your `convex/schema.ts`: + +```ts +import { authSchema } from "./authSchema"; +import { defineSchema } from "convex/server"; + +export default defineSchema({ + ...authSchema, + // Your other tables here +}); +``` + +### Or Use as Reference + +Alternatively, use the generated schema as a reference to manually update your existing schema: + +```ts +// Example: Adding a missing field discovered from generated schema +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + user: defineTable({ + // ... existing fields + twoFactorEnabled: v.optional(v.union(v.null(), v.boolean())), // New field from Better Auth update + // ... rest of your schema + }).index("email_name", ["email", "name"]), + // ... other indexes +}); +``` + +### Adding Custom Indexes + +Better Auth may log warnings about missing indexes for certain queries. You can add custom indexes by extending the generated schema: + +```ts +// convex/schema.ts +import { authSchema } from "./authSchema"; +import { defineSchema } from "convex/server"; + +export default defineSchema({ + ...authSchema, + // Override with custom indexes + user: authSchema.user.index("username", ["username"]), + // Your other tables +}); +``` + +**Note**: `authSchema` table names and field names should not be customized directly. Use Better Auth configuration options to customize the schema, then regenerate to see the expected structure. + +## Credits + +Built on top of [Better Auth](https://www.better-auth.com) and [@convex-dev/better-auth](https://labs.convex.dev/better-auth), optimized for [Convex](https://www.convex.dev). diff --git a/.claude/docs/references/better-auth/experimental.mdx b/.claude/docs/references/better-auth/experimental.mdx new file mode 100644 index 00000000..32a0fb3b --- /dev/null +++ b/.claude/docs/references/better-auth/experimental.mdx @@ -0,0 +1,274 @@ +--- +title: Experimental +description: Experimental features for Convex + Better Auth +--- + + + These features are experimental and may be changed or removed in the future. + + +## JWT Caching + +**Faster page loads and navigation by reusing JWT from cookies.** + +Authenticated queries in SSR require an additional request for a token. This can +slow down initial page load and navigation for frameworks that server render on +in-app navigation. + +JWT caching allows server utilties like `fetchAuthQuery` to utilize the Convex +JWT from request cookies if present and unexpired. + +First, create a utility function for determining whether an error is auth +related. + +```ts title="lib/utils.ts" +import { ConvexError } from "convex/values"; + +export const isAuthError = (error: unknown) => { + // This broadly matches potentially auth related errors, can be rewritten to + // work with your app's own error handling. + const message = + (error instanceof ConvexError && error.data) || + (error instanceof Error && error.message) || + ""; + return /auth/i.test(message); +}; +``` + +Configure `jwtCache` in server utilities. + + + + + +```ts title="lib/auth-server.ts" +import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs"; +import { isAuthError } from "@/lib/utils"; + +export const { fetchAuthQuery } = convexBetterAuthNextJs({ + convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!, + convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!, + jwtCache: { + enabled: true, + isAuthError, + }, +}); +``` + + + + + +```ts title="lib/auth-server.ts" +import { convexBetterAuthReactStart } from "@convex-dev/better-auth/react-start"; +import { isAuthError } from "@/lib/utils"; + +export const { fetchAuthQuery } = convexBetterAuthReactStart({ + convexUrl: process.env.VITE_CONVEX_URL!, + convexSiteUrl: process.env.VITE_CONVEX_SITE_URL!, + jwtCache: { + enabled: true, + isAuthError, + }, +}); +``` + + + + + +## Static JWKS + +**Faster page loads, navigation, and client readiness by speeding up Convex +backend token validation.** + +When a Convex function is called over http (as opposed to the websocket client), +every request must include a token and be validated by the Convex backend. Token +validation is never cached. By default, validation requires the Convex backend +to make two blocking http requests serially: one for OIDC discovery, which +provides the JWKS url, and one for fetching the JWKS from the url. The JWT is +then validated using the JWKS. + +By default the Better Auth Component uses Convex's +[`customJwt`](https://docs.convex.dev/auth/advanced/custom-jwt) to avoid the +OIDC request by providing the JWKS url statically in the auth config. Static +JWKS avoids both calls by providing the JWKS itself as a data uri. + +First, add an internal mutation to get the latest JWKS. A new key may be +written, so this must be a mutation. + +```ts title="convex/functions/auth.ts" +export const getLatestJwks = internalAction({ + args: {}, + handler: async (ctx) => { + const auth = createAuth(ctx); + // This method is added by the Convex Better Auth plugin and is + // available via `auth.api` only, not exposed as a route. + return await auth.api.getLatestJwks(); + }, +}); +``` + +Run the mutation and set the JWKS environment variable from the CLI. + +```bash +npx convex run auth:getLatestJwks | npx convex env set JWKS +``` + +Provide the JWKS environment variable to your auth config and the Convex Better +Auth plugin. + +```ts title="convex/auth.config.ts" +export default { + providers: [getAuthConfigProvider({ jwks: process.env.JWKS })], // [!code ++] +} satisfies AuthConfig; +``` + +```ts title="convex/functions/auth.ts" +export const createAuthOptions = (ctx: GenericCtx) => { + return { + baseURL: siteUrl, + database: authComponent.adapter(ctx), + // ... other auth config + plugins: [ + // ... other plugins + convex({ + authConfig, + jwks: process.env.JWKS, // [!code ++] + }), + ], + } satisfies BetterAuthOptions; +}; +``` + +## AuthBoundary + +The `AuthBoundary` React component is a wrapper that provides error handling for +auth related errors in the client. It subscribes to a session validated user +query for synced handling of session state changes. + +The Convex Component exports a query for use with `AuthBoundary`. + +```ts title="convex/functions/auth.ts" +export const { getAuthUser } = authComponent.clientApi(); // [!code ++] +``` + +Create a utility function for determining whether an error is auth related. + +```ts title="lib/utils.ts" +import { ConvexError } from "convex/values"; + +export const isAuthError = (error: unknown) => { + // This broadly matches potentially auth related errors, can be rewritten to + // work with your app's own error handling. + const message = + (error instanceof ConvexError && error.data) || + (error instanceof Error && error.message) || + ""; + return /auth/i.test(message); +}; +``` + +Add a client component to wrap and configure `AuthBoundary`. + + + +```tsx title="lib/auth-client.tsx" +"use client"; + +import { useRouter } from "next/navigation"; +import { AuthBoundary } from "@convex-dev/better-auth/react"; +import { api } from "@/convex/_generated/api"; +import { isAuthError } from "@/lib/utils"; +import { authClient } from "@/lib/auth-client"; + +export const ClientAuthBoundary = ({ children }: PropsWithChildren) => { + const router = useRouter(); + return ( + router.replace("/sign-in")} + getAuthUserFn={api.auth.getAuthUser} + isAuthError={isAuthError} + > + {children} + + ); +}; +``` + + +```tsx title="lib/auth-client.tsx" +"use client"; + +import { useNavigate } from "@tanstack/react-router"; +import { AuthBoundary } from "@convex-dev/better-auth/react"; +import { api } from "@/convex/_generated/api"; +import { isAuthError } from "@/lib/utils"; +import { authClient } from "@/lib/auth-client"; + +export const ClientAuthBoundary = ({ children }: PropsWithChildren) => { + const navigate = useNavigate(); + return ( + navigate({ to: "/sign-in" })} + getAuthUserFn={api.auth.getAuthUser} + isAuthError={isAuthError} + > + {children} + + ); +}; +``` + + + +Wrap authenticated layout, route, etc. with `ClientAuthBoundary`. Most apps can +just use one. + + + + +```tsx title="app/(auth)/layout.tsx" +import { isAuthenticated } from "@/lib/auth-server"; +import { ClientAuthBoundary } from "@/lib/auth-client"; +import { redirect } from "next/navigation"; +import { PropsWithChildren } from "react"; + +export default async function Layout({ children }: PropsWithChildren) { + if (!(await isAuthenticated())) { + redirect("/sign-in"); + } + return {children}; +} +``` + + + + +```tsx title="src/routes/_authed.tsx" +import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' +import { ClientAuthBoundary } from '@/lib/auth-client' + +export const Route = createFileRoute("/_authed")({ + beforeLoad: ({ context }) => { + if (!context.isAuthenticated) { + console.log("redirecting to /sign-in"); + throw redirect({ to: "/sign-in" }); + } + }, + component: () => { + return ( + + + + ); + }, +}); +``` + + + diff --git a/.claude/docs/references/better-auth/index.mdx b/.claude/docs/references/better-auth/index.mdx new file mode 100644 index 00000000..483e235b --- /dev/null +++ b/.claude/docs/references/better-auth/index.mdx @@ -0,0 +1,151 @@ +--- +title: Getting Started +description: Getting Started with Better Auth and Convex +--- + +## Introduction + +Convex + Better Auth is a [Convex Component](https://www.convex.dev/components) +that provides an integration layer for using [Better +Auth](https://www.better-auth.com) with [Convex](https://www.convex.dev). + +## Prerequisites + +
+ +
+### Create a Convex project + +To use Convex + Better Auth, you'll first need a [Convex](https://www.convex.dev/) project. If you don't have +one, run `npm create convex@latest` to get started, and [check out the +docs](https://docs.convex.dev/home) to learn more about Convex. + +
+ +
+### Run `convex dev` + +Running the cli during setup will initialize your Convex deployment if it +doesn't already exist, and keeps generated types current through the process. +Keep it running. + +```npm +npx convex dev +``` + +
+
+ +## Select your framework + +Installation steps vary by framework. Select yours to get started. + + + +
+ + React + + + React (Vite SPA) +
+ +
+ +
+ + + Expo (React Native) + + Expo (React Native) +
+ +
+ +
+ + TanStack Start + + + TanStack Start +
+ +
+ +
+ + Next.js + + + Next.js +
+
+ +
+ + Svelte + + + + + + + + + + SvelteKit + +
+
+
diff --git a/.claude/docs/references/better-auth/local-install.mdx b/.claude/docs/references/better-auth/local-install.mdx new file mode 100644 index 00000000..587d3560 --- /dev/null +++ b/.claude/docs/references/better-auth/local-install.mdx @@ -0,0 +1,310 @@ +--- +title: Local Install +description: Own your auth. +--- + +Local install gives you full control over your Better Auth schema, allows schema +related configuration to work, and makes it possible to use plugins beyond those +[supported](/supported-plugins) for Convex + Better Auth. It also allows you to +write Convex functions that directly access Better Auth component tables. + +With this approach, the Better Auth plugin is defined in it's own Convex +subdirectory. Installation is a bit different from the default approach, and +includes a schema generation step via Better Auth CLI, similar to the +installation experience with other providers. + +## Installation + + + Before you begin, follow the [Getting Started](/) guide to set up Convex + + Better Auth for your project. Then return here to walk through converting the + default install to a local install. + + +
+
+ + ### Create the component definition + +Create a `convex/betterAuth/convex.config.ts` file to define the component. This +will signal to Convex that the `convex/betterAuth` directory is a locally +installed component. + + ```ts title="convex/betterAuth/convex.config.ts" + import { defineComponent } from "convex/server"; + + const component = defineComponent("betterAuth"); + + export default component; + ``` + +
+ +
+ ### Generate the schema + + Add a static `auth` export to the `convex/betterAuth/auth.ts` file. + + + This file should _only_ have your `auth` export for schema generation, and no + other code. If this file is imported at runtime it will trigger errors due to + missing environment variables. + + + ```ts title="convex/betterAuth/auth.ts" + import { createAuth } from '../auth' + + // Export a static instance for Better Auth schema generation + export const auth = createAuth({} as any) + + ``` + + Generate the schema for the component. + + ```bash + cd convex/betterAuth + npx @better-auth/cli generate -y + ``` + +
+ +
+ ### Split out `createAuthOptions` function + + Code in your component directory needs access to your Better Auth options, + but running `createAuth()` inside of your component directory will trigger + errors from Better Auth due to lack of environment variable access. + + To avoid this, you'll want to have a separate `createAuthOptions` function + that just returns the typed options object. + + ```ts title="convex/functions/auth.ts" + import { + betterAuth, + type BetterAuthOptions, // [!code ++] + } from "better-auth/minimal"; + + // [!code ++:5] + export const createAuthOptions = (ctx: GenericCtx) => { + return { + // ... auth config + } satisfies BetterAuthOptions; + }; + + export const createAuth = (ctx: GenericCtx) => { + // [!code --:3] + return betterAuth({ + // ... auth config + }); + return betterAuth(createAuthOptions(ctx)); // [!code ++] + }; + ``` + +
+ +
+ + ### Export adapter functions + + Export adapter functions for the component. + + ```ts title="convex/betterAuth/adapter.ts" + import { createApi } from "@convex-dev/better-auth"; + import schema from "./schema"; + import { createAuthOptions } from "../auth"; + + export const { + create, + findOne, + findMany, + updateOne, + updateMany, + deleteOne, + deleteMany, + } = createApi(schema, createAuthOptions); + ``` + +
+ +
+ + ### Update component registration + + Update component registration to use the locally installed component. + + ```ts title="convex/convex.config.ts" + import { defineApp } from "convex/server"; + import betterAuth from "@convex-dev/better-auth/convex.config"; // [!code --] + import betterAuth from "./betterAuth/convex.config"; // [!code ++] + + const app = defineApp(); + app.use(betterAuth); + + export default app; + ``` + +
+ +
+ ### Update component config + + Update the component client config to use the local schema. + + ```ts title="convex/functions/auth.ts" + import authSchema from "./betterAuth/schema"; // [!code ++] + + // ... + + export const authComponent = createClient(components.betterAuth); // [!code --] + export const authComponent = createClient( // [!code ++] + components.betterAuth, + // [!code ++:5] + { + local: { + schema: authSchema, + }, + } + ); + + // ... + ``` + +
+ +
+ ### You're done! + + The Better Auth component and schema are now locally defined in your Convex project. + +
+ +
+ +## Usage + +### Updating the schema + +Certain options changes may require schema generation. The Better Auth docs will +often note when this is the case. To regenerate the schema at any time (as it's +generally safe to do), move into the component directory and run the Better Auth +CLI generate command. + +```bash +cd convex/betterAuth +npx @better-auth/cli generate -y +``` + +### Adding custom indexes + +Some database interactions through Better Auth may run queries that don't use an +index. The Better Auth component automatically selects a suitable index for a +given query if one exists, and will log a warning indicating what index should +be added. + +Custom indexes can be added by generating the schema to a secondary file, +importing it `convex/betterAuth/schema.ts` and adding the indexes. This way +custom indexes aren't overwritten when the schema is regenerated. + + + Schema table names and fields should not be customized directly, as any + customizations won't match your Better Auth configuration, and will be + overwritten when the schema is regenerated. Instead, Better Auth schema can be + [customized through + options](https://www.better-auth.com/docs/concepts/database#core-schema). + + +
+
+ + #### Generate the schema + + Generate the schema to a secondary file. + + ```bash + cd convex/betterAuth + npx @better-auth/cli generate -y --output generatedSchema.ts + ``` + +
+ +
+ #### Update the final schema + + Delete the contents of `schema.ts` and replace with table definitions from the + generated schema. + + ```ts title="convex/betterAuth/schema.ts" + import { defineSchema } from "convex/server"; + import { tables } from "./generatedSchema"; + + const schema = defineSchema({ + ...tables, + // Spread the generated schema and add a custom index + user: tables.user.index("custom_index", ["field1", "field2"]), + }); + + export default schema; + ``` + +
+
+ +### Accessing component data + +Convex functions within your Better Auth component directory can access the +component's tables directly, and can then be run from outside the component via +`ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction`. + +Note that if an internal function is defined in a component, it will not be +accessible from outside the component, so functions that need to run outside the +component cannot be internal. While normally public functions are exposed to the +internet, **Convex functions exported by a component are never exposed to the +internet, even if they are public**. + + + If a function in a component is called from outside the component, the return + type won't be inferred unless a return validator is provided. + + +```ts title="convex/betterAuth/someFile.ts" +import { query, mutation } from "./_generated/server"; +import { doc } from "convex-helpers/validators"; +import schema from "./schema"; +import { v } from "convex/values"; + +// This is accessible from outside the component +export const someFunction = query({ + args: { sessionId: v.id("session") }, + // Add a return validator so the return value is typed when + // called from outside the component. + returns: v.union(v.null(), doc(schema, "session")), + handler: async (ctx, args) => { + return await ctx.db.get(args.sessionId); + }, +}); + +// This is not accessible from outside the component. +export const someInternalFunction = internalQuery({ + args: { sessionId: v.id("session") }, + handler: async (ctx, args) => { + return await ctx.db.get(args.sessionId); + }, +}); +``` + +These functions can now be called from a parent component (or app). + +```ts title="convex/someFile.ts" +import { query } from "./_generated/server"; +import { components } from "./_generated/api"; +import { v } from "convex/values"; + +export const someFunction = query({ + args: { sessionId: v.id("session") }, + handler: async (ctx, args) => { + return await ctx.runQuery(components.betterAuth.someFile.someFunction, { + sessionId: args.sessionId, + }); + }, +}); +``` diff --git a/.claude/docs/references/better-auth/next.mdx b/.claude/docs/references/better-auth/next.mdx new file mode 100644 index 00000000..30a0caf0 --- /dev/null +++ b/.claude/docs/references/better-auth/next.mdx @@ -0,0 +1,380 @@ +--- +title: Next.js +description: Install and configure Convex + Better Auth for Next.js. +--- + + + Check out a complete Convex + Better Auth example with Next.js in the [GitHub + repo](https://github.com/get-convex/better-auth/tree/main/examples/next). + + +## Installation + +
+ +
+ ### Install packages + + Install the component, a pinned version of Better Auth, and ensure the latest version + of Convex. + + This component requires Convex `1.25.0` or later. + + ```npm + npm install convex@latest @convex-dev/better-auth + npm install better-auth@1.4.9 --save-exact + ``` + +
+ +
+ ### Register the component + + Register the Better Auth component in your Convex project. + + ```ts title="convex/convex.config.ts" + import { defineApp } from "convex/server"; + import betterAuth from "@convex-dev/better-auth/convex.config"; + + const app = defineApp(); + app.use(betterAuth); + + export default app; + ``` + +
+ +
+ ### Add Convex auth config + + Add a `convex/auth.config.ts` file to configure Better Auth as an authentication provider. + + ```ts title="convex/auth.config.ts" + import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config"; + import type { AuthConfig } from "convex/server"; + + export default { + providers: [getAuthConfigProvider()], + } satisfies AuthConfig; + ``` + +
+ +
+ ### Set environment variables + + Generate a secret for encryption and generating hashes. Use the command below if you have openssl installed, or generate your own however you like. + + ```shell + npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32) + ``` + + Add your site URL to your Convex deployment. + + ```shell + npx convex env set SITE_URL http://localhost:3000 + ``` + + Add environment variables to the `.env.local` file created by `npx convex dev`. It will be picked up by your framework dev server. + + ```shell title=".env.local" tab="Cloud" + # Deployment used by \`npx convex dev\` + CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name + + NEXT_PUBLIC_CONVEX_URL=https://adjective-animal-123.convex.cloud + + # Same as NEXT_PUBLIC_CONVEX_URL but ends in .site // [!code ++] + NEXT_PUBLIC_CONVEX_SITE_URL=https://adjective-animal-123.convex.site # [!code ++] + + # Your local site URL // [!code ++] + NEXT_PUBLIC_SITE_URL=http://localhost:3000 # [!code ++] + ``` + + ```shell title=".env.local" tab="Self hosted" + # Deployment used by \`npx convex dev\` + CONVEX_DEPLOYMENT=dev:adjective-animal-123 # team: team-name, project: project-name + + NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 + + # Will generally be one number higher than NEXT_PUBLIC_CONVEX_URL, + # so if your convex url is :3212, your site url will be :3213 + NEXT_PUBLIC_CONVEX_SITE_URL=http://127.0.0.1:3211 # [!code ++] + + # Your local site URL // [!code ++] + NEXT_PUBLIC_SITE_URL=http://localhost:3000 # [!code ++] + ``` + +
+ +
+ ### Create a Better Auth instance + + Create a Better Auth instance and initialize the component. + + Some Typescript errors will show until you save the file. + + ```ts title="convex/functions/auth.ts" + import { createClient, type GenericCtx } from "@convex-dev/better-auth"; + import { convex } from "@convex-dev/better-auth/plugins"; + import { components } from "./_generated/api"; + import { DataModel } from "./_generated/dataModel"; + import { query } from "./_generated/server"; + import { betterAuth } from "better-auth/minimal"; + import authConfig from "./auth.config"; + + const siteUrl = process.env.SITE_URL!; + + // The component client has methods needed for integrating Convex with Better Auth, + // as well as helper methods for general use. + export const authComponent = createClient(components.betterAuth); + + export const createAuth = (ctx: GenericCtx) => { + return betterAuth({ + baseURL: siteUrl, + database: authComponent.adapter(ctx), + // Configure simple, non-verified email/password to get started + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + }, + plugins: [ + // The Convex plugin is required for Convex compatibility + convex({ authConfig }), + ], + }) + } + + // Example function for getting the current user + // Feel free to edit, omit, etc. + export const getCurrentUser = query({ + args: {}, + handler: async (ctx) => { + return authComponent.getAuthUser(ctx); + }, + }); + ``` + +
+ +
+ ### Create a Better Auth client instance + + Create a Better Auth client instance for interacting with the Better Auth server from your client. + + ```ts title="src/lib/auth-client.ts" + import { createAuthClient } from "better-auth/react"; + import { convexClient } from "@convex-dev/better-auth/client/plugins"; + + export const authClient = createAuthClient({ + plugins: [convexClient()], + }); + ``` + +
+ +
+ ### Configure Next.js server utilities + + Configure a set of helper functions for authenticated SSR, server functions, and route handlers. + + ```ts title="src/lib/auth-server.ts" + import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs"; + + export const { + handler, + preloadAuthQuery, + isAuthenticated, + getToken, + fetchAuthQuery, + fetchAuthMutation, + fetchAuthAction, + } = convexBetterAuthNextJs({ + convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!, + convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!, + }); + ``` + +
+ +
+ ### Mount handlers + + Register Better Auth route handlers on your Convex deployment. + + ```ts title="convex/http.ts" + import { httpRouter } from "convex/server"; + import { authComponent, createAuth } from "./auth"; + + const http = httpRouter(); + + authComponent.registerRoutes(http, createAuth); + + export default http; + ``` + + Set up route handlers to proxy auth requests from Next.js to your Convex deployment. + + ```ts title="app/api/auth/[...all]/route.ts" + import { handler } from "@/lib/auth-server"; + + export const { GET, POST } = handler; + + ``` + +
+ +
+ ### Set up Convex client provider + + Wrap your app with the `ConvexBetterAuthProvider` component. This replaces the `ConvexProvider` component. + + ```ts title="app/ConvexClientProvider.tsx" + "use client"; + + import { ReactNode } from "react"; + import { ConvexReactClient } from "convex/react"; + import { authClient } from "@/lib/auth-client"; + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + + const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + export function ConvexClientProvider({ + children, + initialToken, + }: { + children: ReactNode; + initialToken?: string | null; + }) { + return ( + + {children} + + ); + } + ``` + +
+
+ +### You're done! + +You're now ready to start using Better Auth with Convex. + +## Usage + +Check out the [Basic Usage](/basic-usage) guide for more information on general +usage. Below are usage notes specific to Next.js. + +### SSR with server components + +Convex queries can be preloaded in server components and rendered in client +components via `preloadAuthQuery` and `usePreloadedAuthQuery`. + +Preloading in a server component: + +```tsx title="app/(auth)/(dashboard)/page.tsx" +import { preloadAuthQuery } from "@/lib/auth-server"; +import { api } from "@/convex/_generated/api"; + +const Page = async () => { + const [preloadedUserQuery] = await Promise.all([ + preloadAuthQuery(api.auth.getCurrentUser), + // Load multiple queries in parallel if needed + ]); + + return ( +
+
+
+ ); +}; + +export default Page; +``` + +Rendering preloaded data in a client component: + +```tsx title="app/(auth)/(dashboard)/header.tsx" +import { usePreloadedAuthQuery } from "@convex-dev/better-auth/nextjs/client"; +import { api } from "@/convex/_generated/api"; + +export const Header = ({ + preloadedUserQuery, +}: { + preloadedUserQuery: Preloaded; +}) => { + const user = usePreloadedAuthQuery(preloadedUserQuery); + return ( +
+

{user?.name}

+
+ ); +}; + +export default Header; +``` + +### Using Better Auth in server code + +Better Auth's +[`auth.api` methods](https://www.better-auth.com/docs/concepts/api) would +normally run in your Next.js server code, but with Convex being your backend, +these methods need to run in a Convex function. The Convex function can then be +called from the client via hooks like `useMutation` or in server functions and +other server code using one of the auth-server utilities like +`fetchAuthMutation`. Authentication is handled automatically using session +cookies. + +Here's an example using the `changePassword` method. The Better Auth `auth.api` +method is called inside of a Convex mutation, because we know this function +needs write access. For reads a query function can be used. + +```ts title="convex/users.ts" +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; +import { createAuth, authComponent } from "./auth"; + +export const updateUserPassword = mutation({ + args: { + currentPassword: v.string(), + newPassword: v.string(), + }, + handler: async (ctx, args) => { + const { auth, headers } = await authComponent.getAuth(createAuth, ctx); + await auth.api.changePassword({ + body: { + currentPassword: args.currentPassword, + newPassword: args.newPassword, + }, + headers, + }); + }, +}); +``` + +Here we call the mutation from a server action. + +```ts title="app/actions.ts" +"use server"; + +import { fetchAuthMutation } from "@/lib/auth-server"; +import { api } from "../convex/_generated/api"; + +// Authenticated mutation via server function +export async function updatePassword({ + currentPassword, + newPassword, +}: { + currentPassword: string; + newPassword: string; +}) { + await fetchAuthMutation(api.users.updatePassword, { + currentPassword, + newPassword, + }); +} +``` diff --git a/.claude/docs/references/better-auth/triggers.mdx b/.claude/docs/references/better-auth/triggers.mdx new file mode 100644 index 00000000..df397805 --- /dev/null +++ b/.claude/docs/references/better-auth/triggers.mdx @@ -0,0 +1,61 @@ +--- +title: Triggers +description: Run transactional callbacks when auth data changes +--- + +Triggers are a Convex-first approach to running mutations when your Better Auth +data changes. Better Auth already supports this behavior for some tables through +`databaseHooks` configuration, but database hooks cannot currently run in the +same transaction as the original operation. + +Triggers run in the same transaction as the original operation, and work on any +table in your Better Auth schema. + +## Configuration + +To enable triggers, pass the `triggers` option to the component client config. A +trigger config object has table names as keys, and each table name can be +assigned an object with any of `onCreate`, `onUpdate`, or `onDelete` hooks. +Throwing an error in a trigger will stop the original operation from committing. + + + A single Better Auth endpoint or `auth.api` call can perform multiple database + interactions. Throwing an error in a trigger will only ensure the database + operation that triggered will fail, but any previous operations will still + commit. + + +```ts title="convex/functions/auth.ts" +import { DataModel } from "./_generated/dataModel"; +import { components } from "./_generated/api"; // [!code --] +import { components, internal } from "./_generated/api"; // [!code ++] +import { createClient } from "@convex-dev/better-auth"; // [!code --] +import { createClient, type AuthFunctions } from "@convex-dev/better-auth"; // [!code ++] + +const authFunctions: AuthFunctions = internal.auth; // [!code ++] + +export const authComponent = createClient(components.betterAuth); // [!code --] +// [!code ++:20] +export const authComponent = createClient(components.betterAuth, { + authFunctions, + triggers: { + user: { + onCreate: async (ctx, doc) => { + await ctx.db.insert("posts", { + title: "Hello, world!", + authId: doc._id, + }); + }, + onUpdate: async (ctx, newDoc, oldDoc) => { + // Both old and new documents are available so you can compare and detect + // changes - you can ignore oldDoc if you don't need it. + }, + onDelete: async (ctx, doc) => { + // The entire deleted document is available + }, + }, + }, +}); + +export const { onCreate, onUpdate, onDelete } = authComponent.triggersApi(); // [!code ++] +``` diff --git a/.claude/docs/references/convex/best-practices.mdx b/.claude/docs/references/convex/best-practices.mdx new file mode 100644 index 00000000..68849274 --- /dev/null +++ b/.claude/docs/references/convex/best-practices.mdx @@ -0,0 +1,924 @@ +# Best Practices + +This is a list of best practices and common anti-patterns around using Convex. We recommend going through this list before broadly releasing your app to production. You may choose to try using all of these best practices from the start, or you may wait until you've gotten major parts of your app working before going through and adopting the best practices here. + +## Await all Promises[​](#await-all-promises "Direct link to Await all Promises") + +### Why?[​](#why "Direct link to Why?") + +Convex functions use async / await. If you don't await all your promises (e.g. `await ctx.scheduler.runAfter`, `await ctx.db.patch`), you may run into unexpected behavior (e.g. failing to schedule a function) or miss handling errors. + +### How?[​](#how "Direct link to How?") + +We recommend the [no-floating-promises](https://typescript-eslint.io/rules/no-floating-promises/) rule of typescript-eslint. + +## Avoid `.filter` on database queries[​](#avoid-filter-on-database-queries "Direct link to avoid-filter-on-database-queries") + +### Why?[​](#why-1 "Direct link to Why?") + +Filtering in code instead of using the `.filter` syntax has the same performance, and is generally easier code to write. Conditions in `.withIndex` or `.withSearchIndex` are more efficient than `.filter` or filtering in code, so almost all uses of `.filter` should either be replaced with a `.withIndex` or `.withSearchIndex` condition, or written as TypeScript code. + +Read through the [indexes documentation](/database/reading-data/indexes/indexes-and-query-perf.md) for an overview of how to define indexes and how they work. + +### Examples[​](#examples "Direct link to Examples") + +convex/messages.ts + +TS + +``` +// ❌ +const tomsMessages = ctx.db + .query("messages") + .filter((q) => q.eq(q.field("author"), "Tom")) + .collect(); + +// ✅ +// Option 1: Use an index +const tomsMessages = await ctx.db + .query("messages") + .withIndex("by_author", (q) => q.eq("author", "Tom")) + .collect(); + +// Option 2: Filter in code +const allMessages = await ctx.db.query("messages").collect(); +const tomsMessages = allMessages.filter((m) => m.author === "Tom"); +``` + +### How?[​](#how-1 "Direct link to How?") + +Search for `.filter` in your Convex codebase — a regex like `\.filter\(\(?q` will probably find all the ones on database queries. + +Decide whether they should be replaced with a `.withIndex` condition — per [this section](/understanding/best-practices/.md#only-use-collect-with-a-small-number-of-results), if you are filtering over a large (1000+) or potentially unbounded number of documents, you should use an index. If not using a `.withIndex` / `.withSearchIndex` condition, consider replacing them with a filter in code for more readability and flexibility. + +See [this article](https://stack.convex.dev/complex-filters-in-convex) for more strategies for filtering. + +### Exceptions[​](#exceptions "Direct link to Exceptions") + +Using `.filter` on a paginated query (`.paginate`) has advantages over filtering in code. The paginated query will return the number of documents requested, including the `.filter` condition, so filtering in code afterwards can result in a smaller page or even an empty page. Using `.withIndex` on a paginated query will still be more efficient than a `.filter`. + +## Only use `.collect` with a small number of results[​](#only-use-collect-with-a-small-number-of-results "Direct link to only-use-collect-with-a-small-number-of-results") + +### Why?[​](#why-2 "Direct link to Why?") + +All results returned from `.collect` count towards database bandwidth (even ones filtered out by `.filter`). It also means that if any document in the result changes, the query will re-run or the mutation will hit a conflict. + +If there's a chance the number of results is large (say 1000+ documents), you should use an index to filter the results further before calling `.collect`, or find some other way to avoid loading all the documents such as using pagination, denormalizing data, or changing the product feature. + +### Example[​](#example "Direct link to Example") + +**Using an index:** + +convex/movies.ts + +TS + +``` +// ❌ -- potentially unbounded +const allMovies = await ctx.db.query("movies").collect(); +const moviesByDirector = allMovies.filter( + (m) => m.director === "Steven Spielberg", +); + +// ✅ -- small number of results, so `collect` is fine +const moviesByDirector = await ctx.db + .query("movies") + .withIndex("by_director", (q) => q.eq("director", "Steven Spielberg")) + .collect(); +``` + +**Using pagination:** + +convex/movies.ts + +TS + +``` +// ❌ -- potentially unbounded +const watchedMovies = await ctx.db + .query("watchedMovies") + .withIndex("by_user", (q) => q.eq("user", "Tom")) + .collect(); + +// ✅ -- using pagination, showing recently watched movies first +const watchedMovies = await ctx.db + .query("watchedMovies") + .withIndex("by_user", (q) => q.eq("user", "Tom")) + .order("desc") + .paginate(paginationOptions); +``` + +**Using a limit or denormalizing:** + +convex/movies.ts + +TS + +``` +// ❌ -- potentially unbounded +const watchedMovies = await ctx.db + .query("watchedMovies") + .withIndex("by_user", (q) => q.eq("user", "Tom")) + .collect(); +const numberOfWatchedMovies = watchedMovies.length; + +// ✅ -- Show "99+" instead of needing to load all documents +const watchedMovies = await ctx.db + .query("watchedMovies") + .withIndex("by_user", (q) => q.eq("user", "Tom")) + .take(100); +const numberOfWatchedMovies = + watchedMovies.length === 100 ? "99+" : watchedMovies.length.toString(); + +// ✅ -- Denormalize the number of watched movies in a separate table +const watchedMoviesCount = await ctx.db + .query("watchedMoviesCount") + .withIndex("by_user", (q) => q.eq("user", "Tom")) + .unique(); +``` + +### How?[​](#how-2 "Direct link to How?") + +Search for `.collect` in your Convex codebase (a regex like `\.collect\(` will probably find these). And think through whether the number of results is small. This function health page in the dashboard can also help surface these. + +The [aggregate component](https://www.npmjs.com/package/@convex-dev/aggregate) or [database triggers](https://stack.convex.dev/triggers) can be helpful patterns for denormalizing data. + +### Exceptions[​](#exceptions-1 "Direct link to Exceptions") + +If you're doing something that requires loading a large number of documents (e.g. performing a migration, making a summary), you may want to use an action to load them in batches via separate queries / mutations. + +## Check for redundant indexes[​](#check-for-redundant-indexes "Direct link to Check for redundant indexes") + +### Why?[​](#why-3 "Direct link to Why?") + +Indexes like `by_foo` and `by_foo_and_bar` are usually redundant (you only need `by_foo_and_bar`). Reducing the number of indexes saves on database storage and reduces the overhead of writing to the table. + +convex/teams.ts + +TS + +``` +// ❌ +const allTeamMembers = await ctx.db + .query("teamMembers") + .withIndex("by_team", (q) => q.eq("team", teamId)) + .collect(); +const currentUserId = /* get current user id from `ctx.auth` */ +const currentTeamMember = await ctx.db + .query("teamMembers") + .withIndex("by_team_and_user", (q) => + q.eq("team", teamId).eq("user", currentUserId), + ) + .unique(); + +// ✅ +// Just don't include a condition on `user` when querying for results on `team` +const allTeamMembers = await ctx.db + .query("teamMembers") + .withIndex("by_team_and_user", (q) => q.eq("team", teamId)) + .collect(); +const currentUserId = /* get current user id from `ctx.auth` */ +const currentTeamMember = await ctx.db + .query("teamMembers") + .withIndex("by_team_and_user", (q) => + q.eq("team", teamId).eq("user", currentUserId), + ) + .unique(); +``` + +### How?[​](#how-3 "Direct link to How?") + +Look through your indexes, either in your `schema.ts` file or in the dashboard, and look for any indexes where one is a prefix of another. + +### Exceptions[​](#exceptions-2 "Direct link to Exceptions") + +`.index("by_foo", ["foo"])` is really an index on the properties `foo` and `_creationTime`, while `.index("by_foo_and_bar", ["foo", "bar"])` is an index on the properties `foo`, `bar`, and `_creationTime`. If you have queries that need to be sorted by `foo` and then `_creationTime`, then you need both indexes. + +For example, `.index("by_channel", ["channel"])` on a table of messages can be used to query for the most recent messages in a channel, but `.index("by_channel_and_author", ["channel", "author"])` could not be used for this since it would first sort the messages by `author`. + +## Use argument validators for all public functions[​](#use-argument-validators-for-all-public-functions "Direct link to Use argument validators for all public functions") + +### Why?[​](#why-4 "Direct link to Why?") + +Public functions can be called by anyone, including potentially malicious attackers trying to break your app. [Argument validators](/functions/validation.md) (as well as return value validators) help ensure you're getting the traffic you expect. + +### Example[​](#example-1 "Direct link to Example") + +convex/movies.ts + +TS + +``` +// ❌ -- `id` and `update` are not validated, so a client could pass +// any Convex value (the type at runtime could mismatch the +// TypeScript type). In particular, `update` could contain +// fields other than `title` and `director`. +export const updateMovie = mutation({ + handler: async ( + ctx, + { + id, + update, + }: { + id: Id<"movies">; + update: Pick, "title" | "director">; + }, + ) => { + await ctx.db.patch("movies", id, update); + }, +}); + +// ✅ -- This can only be called with an ID from the movies table, +// and an `update` object with only the `title`/`director` fields +export const updateMovie = mutation({ + args: { + id: v.id("movies"), + update: v.object({ + title: v.string(), + director: v.string(), + }), + }, + handler: async (ctx, { id, update }) => { + await ctx.db.patch("movies", id, update); + }, +}); +``` + +### How?[​](#how-4 "Direct link to How?") + +Search for `query`, `mutation`, and `action` in your Convex codebase, and ensure that all of them have argument validators (and optionally return value validators). + +You can also check automatically that your functions have argument validators with the [`@convex-dev/require-argument-validators` ESLint rule](/eslint.md#require-argument-validators). + +If you use HTTP actions, you may want to use an argument validation library like [Zod](https://zod.dev) to validate that the HTTP request is the shape you expect. + +## Use some form of access control for all public functions[​](#use-some-form-of-access-control-for-all-public-functions "Direct link to Use some form of access control for all public functions") + +### Why?[​](#why-5 "Direct link to Why?") + +Public functions can be called by anyone, including potentially malicious attackers trying to break your app. If portions of your app should only be accessible when the user is signed in, make sure all these Convex functions check that `ctx.auth.getUserIdentity()` is set. + +You may also have specific checks, like only loading messages that were sent to or from the current user, which you'll want to apply in every relevant public function. + +Favoring more granular functions like `setTeamOwner` over `updateTeam` allows more granular checks for which users can do what. + +Access control checks should either use `ctx.auth.getUserIdentity()` or a function argument that is unguessable (e.g. a UUID, or a Convex ID, provided that this ID is never exposed to any client but the one user). In particular, don't use a function argument which could be spoofed (e.g. email) for access control checks. + +### Example[​](#example-2 "Direct link to Example") + +convex/teams.ts + +TS + +``` +// ❌ -- no checks! anyone can update any team if they get the ID +export const updateTeam = mutation({ + args: { + id: v.id("teams"), + update: v.object({ + name: v.optional(v.string()), + owner: v.optional(v.id("users")), + }), + }, + handler: async (ctx, { id, update }) => { + await ctx.db.patch("teams", id, update); + }, +}); + +// ❌ -- checks access, but uses `email` which could be spoofed +export const updateTeam = mutation({ + args: { + id: v.id("teams"), + update: v.object({ + name: v.optional(v.string()), + owner: v.optional(v.id("users")), + }), + email: v.string(), + }, + handler: async (ctx, { id, update, email }) => { + const teamMembers = /* load team members */ + if (!teamMembers.some((m) => m.email === email)) { + throw new Error("Unauthorized"); + } + await ctx.db.patch("teams", id, update); + }, +}); + +// ✅ -- checks access, and uses `ctx.auth`, which cannot be spoofed +export const updateTeam = mutation({ + args: { + id: v.id("teams"), + update: v.object({ + name: v.optional(v.string()), + owner: v.optional(v.id("users")), + }), + }, + handler: async (ctx, { id, update }) => { + const user = await ctx.auth.getUserIdentity(); + if (user === null) { + throw new Error("Unauthorized"); + } + const isTeamMember = /* check if user is a member of the team */ + if (!isTeamMember) { + throw new Error("Unauthorized"); + } + await ctx.db.patch("teams", id, update); + }, +}); + +// ✅ -- separate functions which have different access control +export const setTeamOwner = mutation({ + args: { + id: v.id("teams"), + owner: v.id("users"), + }, + handler: async (ctx, { id, owner }) => { + const user = await ctx.auth.getUserIdentity(); + if (user === null) { + throw new Error("Unauthorized"); + } + const isTeamOwner = /* check if user is the owner of the team */ + if (!isTeamOwner) { + throw new Error("Unauthorized"); + } + await ctx.db.patch("teams", id, { owner: owner }); + }, +}); + +export const setTeamName = mutation({ + args: { + id: v.id("teams"), + name: v.string(), + }, + handler: async (ctx, { id, name }) => { + const user = await ctx.auth.getUserIdentity(); + if (user === null) { + throw new Error("Unauthorized"); + } + const isTeamMember = /* check if user is a member of the team */ + if (!isTeamMember) { + throw new Error("Unauthorized"); + } + await ctx.db.patch("teams", id, { name: name }); + }, +}); +``` + +### How?[​](#how-5 "Direct link to How?") + +Search for `query`, `mutation`, `action`, and `httpAction` in your Convex codebase, and ensure that all of them have some form of access control. [Custom functions](https://github.com/get-convex/convex-helpers/blob/main/packages/convex-helpers/README.md#custom-functions) like [`authenticatedQuery`](https://stack.convex.dev/custom-functions#modifying-the-ctx-argument-to-a-server-function-for-user-auth) can be helpful. + +Some apps use Row Level Security (RLS) to check access to each document automatically whenever it's loaded, as described in [this article](https://stack.convex.dev/row-level-security). Alternatively, you can check access in each Convex function instead of checking access for each document. + +Helper functions for common checks and common operations can also be useful -- e.g. `isTeamMember`, `isTeamAdmin`, `loadTeam` (which throws if the current user does not have access to the team). + +## Only schedule and `ctx.run*` internal functions[​](#only-schedule-and-ctxrun-internal-functions "Direct link to only-schedule-and-ctxrun-internal-functions") + +### Why?[​](#why-6 "Direct link to Why?") + +Public functions can be called by anyone, including potentially malicious attackers trying to break your app, and should be carefully audited to ensure they can't be used maliciously. Functions that are only called within Convex can be marked as internal, and relax these checks since Convex will ensure that internal functions can only be called within Convex. + +### How?[​](#how-6 "Direct link to How?") + +Search for `ctx.runQuery`, `ctx.runMutation`, and `ctx.runAction` in your Convex codebase. Also search for `ctx.scheduler` and check the `crons.ts` file. Ensure all of these use `internal.foo.bar` functions instead of `api.foo.bar` functions. + +If you have code you want to share between a public Convex function and an internal Convex function, create a helper function that can be called from both. The public function will likely have additional access control checks. + +Alternatively, make sure that `api` from `_generated/api.ts` is never used in your Convex functions directory. + +### Examples[​](#examples-1 "Direct link to Examples") + +convex/teams.ts + +TS + +``` +// ❌ -- using `api` +export const sendMessage = mutation({ + args: { + body: v.string(), + author: v.string(), + }, + handler: async (ctx, { body, author }) => { + // add message to the database + }, +}); + +// crons.ts +crons.daily( + "send daily reminder", + { hourUTC: 17, minuteUTC: 30 }, + api.messages.sendMessage, + { author: "System", body: "Share your daily update!" }, +); + +// ✅ Using `internal` +import { MutationCtx } from './_generated/server'; +async function sendMessageHelper( + ctx: MutationCtx, + args: { body: string; author: string }, +) { + // add message to the database +} + +export const sendMessage = mutation({ + args: { + body: v.string(), + }, + handler: async (ctx, { body }) => { + const user = await ctx.auth.getUserIdentity(); + if (user === null) { + throw new Error("Unauthorized"); + } + await sendMessageHelper(ctx, { body, author: user.name ?? "Anonymous" }); + }, +}); + +export const sendInternalMessage = internalMutation({ + args: { + body: v.string(), + // don't need to worry about `author` being spoofed since this is an internal function + author: v.string(), + }, + handler: async (ctx, { body, author }) => { + await sendMessageHelper(ctx, { body, author }); + }, +}); + +// crons.ts +crons.daily( + "send daily reminder", + { hourUTC: 17, minuteUTC: 30 }, + internal.messages.sendInternalMessage, + { author: "System", body: "Share your daily update!" }, +); +``` + +## Use helper functions to write shared code[​](#use-helper-functions-to-write-shared-code "Direct link to Use helper functions to write shared code") + +### Why?[​](#why-7 "Direct link to Why?") + +Most logic should be written as plain TypeScript functions, with the `query`, `mutation`, and `action` wrapper functions being a thin wrapper around one or more helper function. + +Concretely, most of your code should live in a directory like `convex/model`, and your public API, which is defined with `query`, `mutation`, and `action`, should have very short functions that mostly just call into `convex/model`. + +Organizing your code this way makes several of the refactors mentioned in this list easier to do. + +See the [TypeScript page](/understanding/best-practices/typescript.md) for useful types. + +### Example[​](#example-3 "Direct link to Example") + +**❌** This example overuses `ctx.runQuery` and `ctx.runMutation`, which is discussed more in the [Avoid sequential `ctx.runMutation` / `ctx.runQuery` from actions](/understanding/best-practices/.md#avoid-sequential-ctxrunmutation--ctxrunquery-calls-from-actions) section. + +convex/users.ts + +TS + +``` +export const getCurrentUser = query({ + args: {}, + handler: async (ctx) => { + const userIdentity = await ctx.auth.getUserIdentity(); + if (userIdentity === null) { + throw new Error("Unauthorized"); + } + const user = /* query ctx.db to load the user */ + const userSettings = /* load other documents related to the user */ + return { user, settings: userSettings }; + }, +}); +``` + +convex/conversations.ts + +TS + +``` +export const listMessages = query({ + args: { + conversationId: v.id("conversations"), + }, + handler: async (ctx, { conversationId }) => { + const user = await ctx.runQuery(api.users.getCurrentUser); + const conversation = await ctx.db.get("conversations", conversationId); + if (conversation === null || !conversation.members.includes(user._id)) { + throw new Error("Unauthorized"); + } + const messages = /* query ctx.db to load the messages */ + return messages; + }, +}); + +export const summarizeConversation = action({ + args: { + conversationId: v.id("conversations"), + }, + handler: async (ctx, { conversationId }) => { + const messages = await ctx.runQuery(api.conversations.listMessages, { + conversationId, + }); + const summary = /* call some external service to summarize the conversation */ + await ctx.runMutation(api.conversations.addSummary, { + conversationId, + summary, + }); + }, +}); +``` + +**✅** Most of the code here is now in the `convex/model` directory. The API for this application is in `convex/conversations.ts`, which contains very little code itself. + +convex/model/users.ts + +TS + +``` +import { QueryCtx } from '../_generated/server'; + +export async function getCurrentUser(ctx: QueryCtx) { + const userIdentity = await ctx.auth.getUserIdentity(); + if (userIdentity === null) { + throw new Error("Unauthorized"); + } + const user = /* query ctx.db to load the user */ + const userSettings = /* load other documents related to the user */ + return { user, settings: userSettings }; +} +``` + +convex/model/conversations.ts + +TS + +``` +import { QueryCtx, MutationCtx } from '../_generated/server'; +import * as Users from './users'; + +export async function ensureHasAccess( + ctx: QueryCtx, + { conversationId }: { conversationId: Id<"conversations"> }, +) { + const user = await Users.getCurrentUser(ctx); + const conversation = await ctx.db.get("conversations", conversationId); + if (conversation === null || !conversation.members.includes(user._id)) { + throw new Error("Unauthorized"); + } + return conversation; +} + +export async function listMessages( + ctx: QueryCtx, + { conversationId }: { conversationId: Id<"conversations"> }, +) { + await ensureHasAccess(ctx, { conversationId }); + const messages = /* query ctx.db to load the messages */ + return messages; +} + +export async function addSummary( + ctx: MutationCtx, + { + conversationId, + summary, + }: { conversationId: Id<"conversations">; summary: string }, +) { + await ensureHasAccess(ctx, { conversationId }); + await ctx.db.patch("conversations", conversationId, { summary }); +} + +export async function generateSummary( + messages: Doc<"messages">[], + conversationId: Id<"conversations">, +) { + const summary = /* call some external service to summarize the conversation */ + return summary; +} +``` + +convex/conversations.ts + +TS + +``` +import * as Conversations from './model/conversations'; + +export const addSummary = internalMutation({ + args: { + conversationId: v.id("conversations"), + summary: v.string(), + }, + handler: async (ctx, { conversationId, summary }) => { + await Conversations.addSummary(ctx, { conversationId, summary }); + }, +}); + +export const listMessages = internalQuery({ + args: { + conversationId: v.id("conversations"), + }, + handler: async (ctx, { conversationId }) => { + return Conversations.listMessages(ctx, { conversationId }); + }, +}); + +export const summarizeConversation = action({ + args: { + conversationId: v.id("conversations"), + }, + handler: async (ctx, { conversationId }) => { + const messages = await ctx.runQuery(internal.conversations.listMessages, { + conversationId, + }); + const summary = await Conversations.generateSummary( + messages, + conversationId, + ); + await ctx.runMutation(internal.conversations.addSummary, { + conversationId, + summary, + }); + }, +}); +``` + +## Use `runAction` only when using a different runtime[​](#use-runaction-only-when-using-a-different-runtime "Direct link to use-runaction-only-when-using-a-different-runtime") + +### Why?[​](#why-8 "Direct link to Why?") + +Calling `runAction` has more overhead than calling a plain TypeScript function. It counts as an extra function call with its own memory and CPU usage, while the parent action is doing nothing except waiting for the result. Therefore, `runAction` should almost always be replaced with calling a plain TypeScript function. However, if you want to call code that requires Node.js from a function in the Convex runtime (e.g. using a library that requires Node.js), then you can use `runAction` to call the Node.js code. + +### Example[​](#example-4 "Direct link to Example") + +convex/scrape.ts + +TS + +``` +// ❌ -- using `runAction` +export const scrapeWebsite = action({ + args: { + siteMapUrl: v.string(), + }, + handler: async (ctx, { siteMapUrl }) => { + const siteMap = await fetch(siteMapUrl); + const pages = /* parse the site map */ + await Promise.all( + pages.map((page) => + ctx.runAction(internal.scrape.scrapeSinglePage, { url: page }), + ), + ); + }, +}); +``` + +convex/model/scrape.ts + +TS + +``` +import { ActionCtx } from '../_generated/server'; + +// ✅ -- using a plain TypeScript function +export async function scrapeSinglePage( + ctx: ActionCtx, + { url }: { url: string }, +) { + const page = await fetch(url); + const text = /* parse the page */ + await ctx.runMutation(internal.scrape.addPage, { url, text }); +} +``` + +convex/scrape.ts + +TS + +``` +import * as Scrape from './model/scrape'; + +export const scrapeWebsite = action({ + args: { + siteMapUrl: v.string(), + }, + handler: async (ctx, { siteMapUrl }) => { + const siteMap = await fetch(siteMapUrl); + const pages = /* parse the site map */ + await Promise.all( + pages.map((page) => Scrape.scrapeSinglePage(ctx, { url: page })), + ); + }, +}); +``` + +### How?[​](#how-7 "Direct link to How?") + +Search for `runAction` in your Convex codebase, and see if the function it calls uses the same runtime as the parent function. If so, replace the `runAction` with a plain TypeScript function. You may want to structure your functions so the Node.js functions are in a separate directory so it's easier to spot these. + +## Avoid sequential `ctx.runMutation` / `ctx.runQuery` calls from actions[​](#avoid-sequential-ctxrunmutation--ctxrunquery-calls-from-actions "Direct link to avoid-sequential-ctxrunmutation--ctxrunquery-calls-from-actions") + +### Why?[​](#why-9 "Direct link to Why?") + +Each `ctx.runMutation` or `ctx.runQuery` runs in its own transaction, which means if they're called separately, they may not be consistent with each other. If instead we call a single `ctx.runQuery` or `ctx.runMutation`, we're guaranteed that the results we get are consistent. + +### How?[​](#how-8 "Direct link to How?") + +Audit your calls to `ctx.runQuery` and `ctx.runMutation` in actions. If you see multiple in a row with no other code between them, replace them with a single `ctx.runQuery` or `ctx.runMutation` that handles both things. Refactoring your code to use helper functions will make this easier. + +### Example: Queries[​](#example-queries "Direct link to Example: Queries") + +convex/teams.ts + +TS + +``` +// ❌ -- this assertion could fail if the team changed between running the two queries +const team = await ctx.runQuery(internal.teams.getTeam, { teamId }); +const teamOwner = await ctx.runQuery(internal.teams.getTeamOwner, { teamId }); +assert(team.owner === teamOwner._id); +``` + +convex/teams.ts + +TS + +``` +import * as Teams from './model/teams'; +import * as Users from './model/users'; + +export const sendBillingReminder = action({ + args: { + teamId: v.id("teams"), + }, + handler: async (ctx, { teamId }) => { + // ✅ -- this will always pass + const teamAndOwner = await ctx.runQuery(internal.teams.getTeamAndOwner, { + teamId, + }); + assert(teamAndOwner.team.owner === teamAndOwner.owner._id); + // send a billing reminder email to the owner + }, +}); + +export const getTeamAndOwner = internalQuery({ + args: { + teamId: v.id("teams"), + }, + handler: async (ctx, { teamId }) => { + const team = await Teams.load(ctx, { teamId }); + const owner = await Users.load(ctx, { userId: team.owner }); + return { team, owner }; + }, +}); +``` + +### Example: Loops[​](#example-loops "Direct link to Example: Loops") + +convex/teams.ts + +TS + +``` +import * as Users from './model/users'; + +export const importTeams = action({ + args: { + teamId: v.id("teams"), + }, + handler: async (ctx, { teamId }) => { + // Fetch team members from an external API + const teamMembers = await fetchTeamMemberData(teamId); + + // ❌ This will run a separate mutation for inserting each user, + // which means you lose transaction guarantees like atomicity. + for (const member of teamMembers) { + await ctx.runMutation(internal.teams.insertUser, member); + } + }, +}); +export const insertUser = internalMutation({ + args: { name: v.string(), email: v.string() }, + handler: async (ctx, { name, email }) => { + await Users.insert(ctx, { name, email }); + }, +}); +``` + +convex/teams.ts + +TS + +``` +import * as Users from './model/users'; + +export const importTeams = action({ + args: { + teamId: v.id("teams"), + }, + handler: async (ctx, { teamId }) => { + // Fetch team members from an external API + const teamMembers = await fetchTeamMemberData(teamId); + + // ✅ This action runs a single mutation that inserts all users in the same transaction. + await ctx.runMutation(internal.teams.insertUsers, teamMembers); + }, +}); +export const insertUsers = internalMutation({ + args: { users: v.array(v.object({ name: v.string(), email: v.string() })) }, + handler: async (ctx, { users }) => { + for (const { name, email } of users) { + await Users.insert(ctx, { name, email }); + } + }, +}); +``` + +### Exceptions[​](#exceptions-3 "Direct link to Exceptions") + +If you're intentionally trying to process more data than fits in a single transaction, like running a migration or aggregating data, then it makes sense to have multiple sequential `ctx.runMutation` / `ctx.runQuery` calls. + +Multiple `ctx.runQuery` / `ctx.runMutation` calls are often necessary because the action does a side effect in between them. For example, reading some data, feeding it to an external service, and then writing the result back to the database. + +## Use `ctx.runQuery` and `ctx.runMutation` sparingly in queries and mutations[​](#use-ctxrunquery-and-ctxrunmutation-sparingly-in-queries-and-mutations "Direct link to use-ctxrunquery-and-ctxrunmutation-sparingly-in-queries-and-mutations") + +### Why?[​](#why-10 "Direct link to Why?") + +While these queries and mutations run in the same transaction, and will give consistent results, they have extra overhead compared to plain TypeScript functions. Wanting a TypeScript helper function is much more common than needing `ctx.runQuery` or `ctx.runMutation`. + +### How?[​](#how-9 "Direct link to How?") + +Audit your calls to `ctx.runQuery` and `ctx.runMutation` in queries and mutations. Unless one of the exceptions below applies, replace them with a plain TypeScript function. + +### Exceptions[​](#exceptions-4 "Direct link to Exceptions") + +* If you're using components, these require `ctx.runQuery` or `ctx.runMutation`. +* If you want partial rollback on an error, you will want `ctx.runMutation` instead of a plain TypeScript function. + +convex/messages.ts + +TS + +``` +export const trySendMessage = mutation({ + args: { + body: v.string(), + author: v.string(), + }, + handler: async (ctx, { body, author }) => { + try { + await ctx.runMutation(internal.messages.sendMessage, { body, author }); + } catch (e) { + // Record the failure, but rollback any writes from `sendMessage` + await ctx.db.insert("failures", { + kind: "MessageFailed", + body, + author, + error: `Error: ${e}`, + }); + } + }, +}); +``` + +## Always include the table name when calling `ctx.db` functions[​](#always-include-the-table-name-when-calling-ctxdb-functions "Direct link to always-include-the-table-name-when-calling-ctxdb-functions") + +### Why?[​](#why-11 "Direct link to Why?") + +Since version 1.31.0 of the `convex` NPM package, the `ctx.db` functions accept a table name as the first argument. While this first argument is currently optional, passing the table name adds an additional safeguard which will be required for custom ID generation in the future. + +### Example[​](#example-5 "Direct link to Example") + +convex/movies.ts + +TS + +``` +// ❌ +await ctx.db.get(movieId); +await ctx.db.patch(movieId, { title: "Whiplash" }); +await ctx.db.replace(movieId, { + title: "Whiplash", + director: "Damien Chazelle", + votes: 0, +}); +await ctx.db.delete(movieId); + +// ✅ vvvvvvvv +await ctx.db.get("movies", movieId); +await ctx.db.patch("movies", movieId, { title: "Whiplash" }); +await ctx.db.replace("movies", movieId, { + title: "Whiplash", + director: "Damien Chazelle", + votes: 0, +}); +await ctx.db.delete("movies", movieId); +``` + +### How?[​](#how-10 "Direct link to How?") + +Search for calls of `db.get`, `db.patch`, `db.replace` and `db.delete` in your Convex codebase, and ensure that all of them pass a table name as the first argument. + +You can also check automatically that a table name argument is passed with the [`@convex-dev/explicit-table-ids` ESLint rule](/eslint.md#explicit-table-ids). + +You can migrate existing code automatically by using the autofix in the ESLint rule, or with the `@convex-dev/codemod` standalone tool. + +[Learn more on news.convex.dev →](https://news.convex.dev/db-table-name/) diff --git a/.claude/docs/references/convex/cli.mdx b/.claude/docs/references/convex/cli.mdx new file mode 100644 index 00000000..60732072 --- /dev/null +++ b/.claude/docs/references/convex/cli.mdx @@ -0,0 +1,254 @@ +# CLI + +The Convex command-line interface (CLI) is your interface for managing Convex projects and Convex functions. + +To install the CLI, run: + +``` +npm install convex +``` + +You can view the full list of commands with: + +``` +npx convex +``` + +## Configure[​](#configure "Direct link to Configure") + +### Create a new project[​](#create-a-new-project "Direct link to Create a new project") + +The first time you run + +``` +npx convex dev +``` + +it will ask you to log in your device and create a new Convex project. It will then create: + +1. The `convex/` directory: This is the home for your query and mutation functions. +2. `.env.local` with `CONVEX_DEPLOYMENT` variable: This is the main configuration for your Convex project. It is the name of your development deployment. + +### Recreate project configuration[​](#recreate-project-configuration "Direct link to Recreate project configuration") + +Run + +``` +npx convex dev +``` + +in a project directory without a set `CONVEX_DEPLOYMENT` to configure a new or existing project. + +### Log out[​](#log-out "Direct link to Log out") + +``` +npx convex logout +``` + +Remove the existing Convex credentials from your device, so subsequent commands like `npx convex dev` can use a different Convex account. + +## Develop[​](#develop "Direct link to Develop") + +### Run the Convex dev server[​](#run-the-convex-dev-server "Direct link to Run the Convex dev server") + +``` +npx convex dev +``` + +Watches the local filesystem. When you change a [function](/functions.md) or the [schema](/database/schemas.md), the new versions are pushed to your dev deployment and the [generated types](/generated-api/.md) in `convex/_generated` are updated. By default, logs from your dev deployment are displayed in the terminal. + +It's also possible to [run a Convex deployment locally](/cli/local-deployments.md) for development. + +### Open the dashboard[​](#open-the-dashboard "Direct link to Open the dashboard") + +``` +npx convex dashboard +``` + +Open the [Convex dashboard](/dashboard.md). + +### Open the docs[​](#open-the-docs "Direct link to Open the docs") + +``` +npx convex docs +``` + +Get back to these docs! + +### Run Convex functions[​](#run-convex-functions "Direct link to Run Convex functions") + +``` +npx convex run [args] +``` + +Run a public or internal Convex query, mutation, or action on your development deployment. + +Arguments are specified as a JSON object. + +``` +npx convex run messages:send '{"body": "hello", "author": "me"}' +``` + +Add `--watch` to live update the results of a query. Add `--push` to push local code to the deployment before running the function. + +Use `--prod` to run functions in the production deployment for a project. + +### Tail deployment logs[​](#tail-deployment-logs "Direct link to Tail deployment logs") + +You can choose how to pipe logs from your dev deployment to your console: + +``` +# Show all logs continuously +npx convex dev --tail-logs always + +# Pause logs during deploys to see sync issues (default) +npx convex dev + +# Don't display logs while developing +npx convex dev --tail-logs disable + +# Tail logs without deploying +npx convex logs +``` + +Use `--prod` with `npx convex logs` to tail the prod deployment logs instead. + +### Import data from a file[​](#import-data-from-a-file "Direct link to Import data from a file") + +``` +npx convex import --table +npx convex import .zip +``` + +See description and use-cases: [data import](/database/import-export/import.md). + +### Export data to a file[​](#export-data-to-a-file "Direct link to Export data to a file") + +``` +npx convex export --path +npx convex export --path .zip +npx convex export --include-file-storage --path +``` + +See description and use-cases: [data export](/database/import-export/export.md). + +### Display data from tables[​](#display-data-from-tables "Direct link to Display data from tables") + +``` +npx convex data # lists tables +npx convex data +``` + +Display a simple view of the [dashboard data page](/dashboard/deployments/data.md) in the command line. + +The command supports `--limit` and `--order` flags to change data displayed. For more complex filters, use the dashboard data page or write a [query](/database/reading-data/.md). + +The `npx convex data
` command works with [system tables](/database/advanced/system-tables.md), such as `_storage`, in addition to your own tables. + +### Read and write environment variables[​](#read-and-write-environment-variables "Direct link to Read and write environment variables") + +``` +npx convex env list +npx convex env get +npx convex env set +npx convex env remove +``` + +See and update the deployment environment variables which you can otherwise manage on the dashboard [environment variables settings page](/dashboard/deployments/deployment-settings.md#environment-variables). + +## Deploy[​](#deploy "Direct link to Deploy") + +### Deploy Convex functions to production[​](#deploy-convex-functions-to-production "Direct link to Deploy Convex functions to production") + +``` +npx convex deploy +``` + +The target deployment to push to is determined like this: + +1. If the `CONVEX_DEPLOY_KEY` environment variable is set (typical in CI), then it is the deployment associated with that key. +2. If the `CONVEX_DEPLOYMENT` environment variable is set (typical during local development), then the target deployment is the production deployment of the project that the deployment specified by `CONVEX_DEPLOYMENT` belongs to. This allows you to deploy to your prod deployment while developing against your dev deployment. + +This command will: + +1. Run a command if specified with `--cmd`. The command will have CONVEX\_URL (or similar) environment variable available: + + + + ``` + npx convex deploy --cmd "npm run build" + ``` + + + + You can customize the URL environment variable name with `--cmd-url-env-var-name`: + + + + ``` + npx convex deploy --cmd 'npm run build' --cmd-url-env-var-name CUSTOM_CONVEX_URL + ``` + +2. Typecheck your Convex functions. + +3. Regenerate the [generated code](/generated-api/.md) in the `convex/_generated` directory. + +4. Bundle your Convex functions and their dependencies. + +5. Push your functions, [indexes](/database/reading-data/indexes/.md), and [schema](/database/schemas.md) to production. + +Once this command succeeds the new functions will be available immediately. + +### Deploy Convex functions to a [preview deployment](/production/hosting/preview-deployments.md)[​](#deploy-convex-functions-to-a-preview-deployment "Direct link to deploy-convex-functions-to-a-preview-deployment") + +``` +npx convex deploy +``` + +When run with the `CONVEX_DEPLOY_KEY` environment variable containing a [Preview Deploy Key](/cli/deploy-key-types.md#deploying-to-preview-deployments), this command will: + +1. Create a new Convex deployment. `npx convex deploy` will infer the Git branch name for Vercel, Netlify, GitHub, and GitLab environments, or the `--preview-create` option can be used to customize the name associated with the newly created deployment. + + ``` + npx convex deploy --preview-create my-branch-name + ``` + +2. Run a command if specified with `--cmd`. The command will have CONVEX\_URL (or similar) environment variable available: + + ``` + npx convex deploy --cmd "npm run build" + ``` + + You can customize the URL environment variable name with `--cmd-url-env-var-name`: + + ``` + npx convex deploy --cmd 'npm run build' --cmd-url-env-var-name CUSTOM_CONVEX_URL + ``` + +3. Typecheck your Convex functions. + +4. Regenerate the [generated code](/generated-api/.md) in the `convex/_generated` directory. + +5. Bundle your Convex functions and their dependencies. + +6. Push your functions, [indexes](/database/reading-data/indexes/.md), and [schema](/database/schemas.md) to the deployment. + +7. Run a function specified by `--preview-run` (similar to the `--run` option for `npx convex dev`). + + ``` + npx convex deploy --preview-run myFunction + ``` + +See the [Vercel](/production/hosting/vercel.md#preview-deployments) or [Netlify](/production/hosting/netlify.md#deploy-previews) hosting guide for setting up frontend and backend previews together. + +### Update generated code[​](#update-generated-code "Direct link to Update generated code") + +``` +npx convex codegen +``` + +The [generated code](/generated-api/.md) in the `convex/_generated` directory includes types required for a TypeScript typecheck. This code is generated whenever necessary while running `npx convex dev` and this code should be committed to the repo (your code won't typecheck without it!). + +In the rare cases it's useful to regenerate code (e.g. in CI to ensure that the correct code was checked it) you can use this command. + +Generating code can require communicating with a convex deployment in order to evaluate configuration files in the Convex JavaScript runtime. This doesn't modify the code running on the deployment. diff --git a/.claude/docs/references/convex/client/nextjs.mdx b/.claude/docs/references/convex/client/nextjs.mdx new file mode 100644 index 00000000..028cac3b --- /dev/null +++ b/.claude/docs/references/convex/client/nextjs.mdx @@ -0,0 +1,312 @@ +# Next.js + +[Next.js](https://nextjs.org/) is a React web development framework. When used with Convex, Next.js provides: + +* File-system based routing +* Fast refresh in development +* Font and image optimization + +and more! + +This page covers the App Router variant of Next.js. Alternatively see the [Pages Router](/client/nextjs/pages-router/.md) version of this page. + +## Getting started[​](#getting-started "Direct link to Getting started") + +Follow the [Next.js Quickstart](/quickstart/nextjs.md) to add Convex to a new or existing Next.js project. + +## Calling Convex functions from client code[​](#calling-convex-functions-from-client-code "Direct link to Calling Convex functions from client code") + +To fetch and edit the data in your database from client code, use hooks of the [Convex React library](/client/react.md). + +## [Convex React library documentation](/client/react.md) + +## Server rendering (SSR)[​](#server-rendering-ssr "Direct link to Server rendering (SSR)") + +Next.js automatically renders both Client and Server Components on the server during the initial page load. + +To keep your UI [automatically reactive](/functions/query-functions.md#caching--reactivity--consistency) to changes in your Convex database it needs to use Client Components. The `ConvexReactClient` will maintain a connection to your deployment and will get updates as data changes and that must happen on the client. + +See the dedicated [Server Rendering](/client/nextjs/app-router/server-rendering.md) page for more details about preloading data for Client Components, fetching data and authentication in Server Components, and implementing Route Handlers. + +## Adding authentication[​](#adding-authentication "Direct link to Adding authentication") + +### Client-side only[​](#client-side-only "Direct link to Client-side only") + +The simplest way to add user authentication to your Next.js app is to follow our React-based authentication guides for [Clerk](/auth/clerk.md) or [Auth0](/auth/auth0.md), inside your `app/ConvexClientProvider.tsx` file. For example this is what the file would look like for Auth0: + +app/ConvexClientProvider.tsx + +TS + +``` +"use client"; + +import { Auth0Provider } from "@auth0/auth0-react"; +import { ConvexReactClient } from "convex/react"; +import { ConvexProviderWithAuth0 } from "convex/react-auth0"; +import { ReactNode } from "react"; + +const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + +export function ConvexClientProvider({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} +``` + +Custom loading and logged out views can be built with the helper `Authenticated`, `Unauthenticated` and `AuthLoading` components from `convex/react`, see the [Convex Next.js demo](https://github.com/get-convex/convex-demos/tree/main/nextjs-pages-router/pages/_app.tsx) for an example. + +If only some routes of your app require login, the same helpers can be used directly in page components that do require login instead of being shared between all pages from `app/ConvexClientProvider.tsx`. Share a single [ConvexReactClient](/api/classes/react.ConvexReactClient.md) instance between pages to avoid needing to reconnect to Convex on client-side page navigation. + +### Server and client side[​](#server-and-client-side "Direct link to Server and client side") + +To access user information or load Convex data requiring `ctx.auth` from Server Components, Server Actions, or Route Handlers you need to use the Next.js specific SDKs provided by Clerk and Auth0. + +Additional `.env.local` configuration is needed for these hybrid SDKs. + +#### Clerk[​](#clerk "Direct link to Clerk") + +For an example of using Convex and with Next.js 15, run + +**`npm create convex@latest -- -t nextjs-clerk`** + +**``** + +Otherwise, follow the [Clerk Next.js quickstart](https://clerk.com/docs/quickstarts/nextjs), a guide from Clerk that includes steps for adding `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` to the .env.local file. In Next.js 15, the `` component imported from the `@clerk/nextjs` v6 package functions as both a client and a server context provider so you probably won't need the `ClerkProvider` from `@clerk/clerk-react`. + +#### Auth0[​](#auth0 "Direct link to Auth0") + +See the [Auth0 Next.js](https://auth0.com/docs/quickstart/webapp/nextjs/01-login) guide. + +#### Other providers[​](#other-providers "Direct link to Other providers") + +Convex uses JWT identity tokens on the client for live query subscriptions and running mutations and actions, and on the Next.js backend for running queries, mutations, and actions in server components and API routes. + +Obtain the appropriate OpenID Identity JWT in both locations and you should be able to use any auth provider. See [Custom Auth](https://docs.convex.dev/auth/advanced/custom-auth) for more. + +# Next.js Server Rendering + +Next.js automatically renders both Client and Server Components on the server during the initial page load. + +By default Client Components will not wait for Convex data to be loaded, and your UI will render in a "loading" state. Read on to learn how to preload data during server rendering and how to interact with the Convex deployment from Next.js server-side. + +**Example:** [Next.js App Router](https://github.com/get-convex/convex-demos/tree/main/nextjs-app-router) + +This pages covers the App Router variant of Next.js. + +Next.js Server Rendering support is in beta + +Next.js Server Rendering support is currently a [beta feature](/production/state/.md#beta-features). If you have feedback or feature requests, [let us know on Discord](https://convex.dev/community)! + +## Preloading data for Client Components[​](#preloading-data-for-client-components "Direct link to Preloading data for Client Components") + +If you want to preload data from Convex and leverage Next.js [server rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#server-rendering-strategies), but still retain reactivity after the initial page load, use [`preloadQuery`](/api/modules/nextjs.md#preloadquery) from [`convex/nextjs`](/api/modules/nextjs.md). + +In a [Server Component](https://nextjs.org/docs/app/building-your-application/rendering/server-components) call `preloadQuery`: + +app/TasksWrapper.tsx + +TS + +``` +import { preloadQuery } from "convex/nextjs"; +import { api } from "@/convex/_generated/api"; +import { Tasks } from "./Tasks"; + +export async function TasksWrapper() { + const preloadedTasks = await preloadQuery(api.tasks.list, { + list: "default", + }); + return ; +} +``` + +In a [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) call [`usePreloadedQuery`](/api/modules/react.md#usepreloadedquery): + +app/TasksWrapper.tsx + +TS + +``` +"use client"; + +import { Preloaded, usePreloadedQuery } from "convex/react"; +import { api } from "@/convex/_generated/api"; + +export function Tasks(props: { + preloadedTasks: Preloaded; +}) { + const tasks = usePreloadedQuery(props.preloadedTasks); + // render `tasks`... + return
...
; +} +``` + +[`preloadQuery`](/api/modules/nextjs.md#preloadquery) takes three arguments: + +1. The query reference +2. Optionally the arguments object passed to the query +3. Optionally a [NextjsOptions](/api/modules/nextjs.md#nextjsoptions) object + +`preloadQuery` uses the [`cache: 'no-store'` policy](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching) so any Server Components using it will not be eligible for [static rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#server-rendering-strategies). + +### Using the query result[​](#using-the-query-result "Direct link to Using the query result") + +[`preloadQuery`](/api/modules/nextjs.md#preloadquery) returns an opaque `Preloaded` payload that should be passed through to `usePreloadedQuery`. If you want to use the return value of the query, perhaps to decide whether to even render the Client Component, you can pass the `Preloaded` payload to the [`preloadedQueryResult`](/api/modules/nextjs.md#preloadedqueryresult) function. + +## Using Convex to render Server Components[​](#using-convex-to-render-server-components "Direct link to Using Convex to render Server Components") + +If you need Convex data on the server, you can load data from Convex in your [Server Components](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching), but it will be non-reactive. To do this, use the [`fetchQuery`](/api/modules/nextjs.md#fetchquery) function from `convex/nextjs`: + +app/StaticTasks.tsx + +TS + +``` +import { fetchQuery } from "convex/nextjs"; +import { api } from "@/convex/_generated/api"; + +export async function StaticTasks() { + const tasks = await fetchQuery(api.tasks.list, { list: "default" }); + // render `tasks`... + return
...
; +} +``` + +## Server Actions and Route Handlers[​](#server-actions-and-route-handlers "Direct link to Server Actions and Route Handlers") + +Next.js supports building HTTP request handling routes, similar to Convex [HTTP Actions](/functions/http-actions.md). You can use Convex from a [Server Action](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) or a [Route Handler](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) as you would any other database service. + +To load and edit Convex data in your Server Action or Route Handler, you can use the `fetchQuery`, `fetchMutation` and `fetchAction` functions. + +Here's an example inline Server Action calling a Convex mutation: + +app/example/page.tsx + +TS + +``` +import { api } from "@/convex/_generated/api"; +import { fetchMutation, fetchQuery } from "convex/nextjs"; +import { revalidatePath } from "next/cache"; + +export default async function PureServerPage() { + const tasks = await fetchQuery(api.tasks.list, { list: "default" }); + async function createTask(formData: FormData) { + "use server"; + + await fetchMutation(api.tasks.create, { + text: formData.get("text") as string, + }); + revalidatePath("/example"); + } + // render tasks and task creation form + return
...; +} +``` + +Here's an example Route Handler calling a Convex mutation: + +app/api/route.ts + +TS + +``` +import { NextResponse } from "next/server"; +// Hack for TypeScript before 5.2 +const Response = NextResponse; + +import { api } from "@/convex/_generated/api"; +import { fetchMutation } from "convex/nextjs"; + +export async function POST(request: Request) { + const args = await request.json(); + await fetchMutation(api.tasks.create, { text: args.text }); + return Response.json({ success: true }); +} +``` + +## Server-side authentication[​](#server-side-authentication "Direct link to Server-side authentication") + +To make authenticated requests to Convex during server rendering, pass a JWT token to [`preloadQuery`](/api/modules/nextjs.md#preloadquery) or [`fetchQuery`](/api/modules/nextjs.md#fetchquery) in the third options argument: + +app/TasksWrapper.tsx + +TS + +``` +import { preloadQuery } from "convex/nextjs"; +import { api } from "@/convex/_generated/api"; +import { Tasks } from "./Tasks"; + +export async function TasksWrapper() { + const token = await getAuthToken(); + const preloadedTasks = await preloadQuery( + api.tasks.list, + { list: "default" }, + { token }, + ); + return ; +} +``` + +The implementation of `getAuthToken` depends on your authentication provider. + +* Clerk +* Auth0 + +app/auth.ts + +TS + +``` +import { auth } from "@clerk/nextjs/server"; + +export async function getAuthToken() { + return (await (await auth()).getToken({ template: "convex" })) ?? undefined; +} +``` + +app/auth.ts + +TS + +``` +// You'll need v4.3 or later of @auth0/nextjs-auth0 +import { getSession } from '@auth0/nextjs-auth0'; + +export async function getAuthToken() { + const session = await getSession(); + const idToken = session.tokenSet.idToken; + return idToken; +} +``` + +## Configuring Convex deployment URL[​](#configuring-convex-deployment-url "Direct link to Configuring Convex deployment URL") + +Convex hooks used by Client Components are configured via the `ConvexReactClient` constructor, as shown in the [Next.js Quickstart](/quickstart/nextjs.md). + +To use `preloadQuery`, `fetchQuery`, `fetchMutation` and `fetchAction` in Server Components, Server Actions and Route Handlers you must either: + +1. have `NEXT_PUBLIC_CONVEX_URL` environment variable set to the Convex deployment URL +2. or pass the [`url` option](/api/modules/nextjs.md#nextjsoptions) in the third argument to `preloadQuery`, `fetchQuery`, `fetchMutation` or `fetchAction` + +## Consistency[​](#consistency "Direct link to Consistency") + +[`preloadQuery`](/api/modules/nextjs.md#preloadquery) and [`fetchQuery`](/api/modules/nextjs.md#fetchquery) use the `ConvexHTTPClient` under the hood. This client is stateless. This means that two calls to `preloadQuery` are not guaranteed to return consistent data based on the same database state. This is similar to more traditional databases, but is different from the [guaranteed consistency](/client/react.md#consistency) provided by the `ConvexReactClient`. + +To prevent rendering an inconsistent UI avoid using multiple `preloadQuery` calls on the same page. diff --git a/.claude/docs/references/convex/client/react.mdx b/.claude/docs/references/convex/client/react.mdx new file mode 100644 index 00000000..51080a31 --- /dev/null +++ b/.claude/docs/references/convex/client/react.mdx @@ -0,0 +1,316 @@ +# Convex React + +Convex React is the client library enabling your React application to interact with your Convex backend. It allows your frontend code to: + +1. Call your [queries](/functions/query-functions.md), [mutations](/functions/mutation-functions.md) and [actions](/functions/actions.md) +2. Upload and display files from [File Storage](/file-storage.md) +3. Authenticate users using [Authentication](/auth.md) +4. Implement full text [Search](/search.md) over your data + +The Convex React client is open source and available on [GitHub](https://github.com/get-convex/convex-js). + +Follow the [React Quickstart](/quickstart/react.md) to get started with React using [Vite](https://vitejs.dev/). + +## Installation[​](#installation "Direct link to Installation") + +Convex React is part of the `convex` npm package: + +``` +npm install convex +``` + +## Connecting to a backend[​](#connecting-to-a-backend "Direct link to Connecting to a backend") + +The [`ConvexReactClient`](/api/classes/react.ConvexReactClient.md) maintains a connection to your Convex backend, and is used by the React hooks described below to call your functions. + +First you need to create an instance of the client by giving it your backend deployment URL. See [Configuring Deployment URL](/client/react/deployment-urls.md) on how to pass in the right value: + +``` +import { ConvexProvider, ConvexReactClient } from "convex/react"; + +const convex = new ConvexReactClient("https://.convex.cloud"); +``` + +And then you make the client available to your app by passing it in to a [`ConvexProvider`](/api/modules/react.md#convexprovider) wrapping your component tree: + +``` +reactDOMRoot.render( + + + + + , +); +``` + +## Fetching data[​](#fetching-data "Direct link to Fetching data") + +Your React app fetches data using the [`useQuery`](/api/modules/react.md#usequery) React hook by calling your [queries](/functions/query-functions.md) via an [`api`](/generated-api/api.md#api) object. + +The `npx convex dev` command generates this api object for you in the `convex/_generated/api.js` module to provide better autocompletion in JavaScript and end-to-end type safety in [TypeScript](/understanding/best-practices/typescript.md): + +src/App.tsx + +TS + +``` +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const data = useQuery(api.functions.myQuery); + return data ?? "Loading..."; +} +``` + +The `useQuery` hook returns `undefined` while the data is first loading and afterwards the return value of your query. + +### Query arguments[​](#query-arguments "Direct link to Query arguments") + +Arguments to your query follow the query name: + +src/App.tsx + +TS + +``` +export function App() { + const a = "Hello world"; + const b = 4; + const data = useQuery(api.functions.myQuery, { a, b }); + //... +} +``` + +### Reactivity[​](#reactivity "Direct link to Reactivity") + +The `useQuery` hook makes your app automatically reactive: when the underlying data changes in your database, your component rerenders with the new query result. + +The first time the hook is used it creates a subscription to your backend for a given query and any arguments you pass in. When your component unmounts, the subscription is canceled. + +### Consistency[​](#consistency "Direct link to Consistency") + +Convex React ensures that your application always renders a consistent view of the query results based on a single state of the underlying database. + +Imagine a mutation changes some data in the database, and that 2 different `useQuery` call sites rely on this data. Your app will never render in an inconsistent state where only one of the `useQuery` call sites reflects the new data. + +### Paginating queries[​](#paginating-queries "Direct link to Paginating queries") + +See [Paginating within React Components](/database/pagination.md#paginating-within-react-components). + +### Skipping queries[​](#skipping-queries "Direct link to Skipping queries") + +Advanced: Loading a query conditionally + +With React it can be tricky to dynamically invoke a hook, because hooks cannot be placed inside conditionals or after early returns: + +src/App.tsx + +TS + +``` +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + // the URL `param` might be null + const param = new URLSearchParams(window.location.search).get("param"); + // ERROR! React Hook "useQuery" is called conditionally. React Hooks must + // be called in the exact same order in every component render. + const data = param !== null ? useQuery(api.functions.read, { param }) : null; + //... +} +``` + +For this reason `useQuery` can be "disabled" by passing in `"skip"` instead of its arguments: + +src/App.tsx + +TS + +``` +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const param = new URLSearchParams(window.location.search).get("param"); + const data = useQuery( + api.functions.read, + param !== null ? { param } : "skip", + ); + //... +} +``` + +When `"skip"` is used the `useQuery` doesn't talk to your backend at all and returns `undefined`. + +### One-off queries[​](#one-off-queries "Direct link to One-off queries") + +Advanced: Fetching a query from a callback + +Sometimes you might want to read state from the database in response to a user action, for example to validate given input, without making any changes to the database. In this case you can use a one-off [`query`](/api/classes/react.ConvexReactClient.md#query) call, similarly to calling mutations and actions. + +The async method `query` is exposed on the `ConvexReactClient`, which you can reference in your components via the [`useConvex()`](/api/modules/react.md#useconvex) hook. + +src/App.tsx + +TS + +``` +import { useConvex } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const convex = useConvex(); + return ( + + ); +} +``` + +## Editing data[​](#editing-data "Direct link to Editing data") + +Your React app edits data using the [`useMutation`](/api/modules/react.md#usemutation) React hook by calling your [mutations](/functions/mutation-functions.md). + +The `convex dev` command generates this api object for you in the `convex/_generated/api.js` module to provide better autocompletion in JavaScript and end-to-end type safety in [TypeScript](/understanding/best-practices/typescript.md): + +src/App.tsx + +TS + +``` +import { useMutation } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const doSomething = useMutation(api.functions.doSomething); + return ; +} +``` + +The hook returns an `async` function which performs the call to the mutation. + +### Mutation arguments[​](#mutation-arguments "Direct link to Mutation arguments") + +Arguments to your mutation are passed to the `async` function returned from `useMutation`: + +src/App.tsx + +TS + +``` +export function App() { + const a = "Hello world"; + const b = 4; + const doSomething = useMutation(api.functions.doSomething); + return ; +} +``` + +### Mutation response and error handling[​](#mutation-response-and-error-handling "Direct link to Mutation response and error handling") + +The mutation can optionally return a value or throw errors, which you can [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await): + +src/App.tsx + +TS + +``` +export function App() { + const doSomething = useMutation(api.functions.doSomething); + const onClick = () => { + async function callBackend() { + try { + const result = await doSomething(); + } catch (error) { + console.error(error); + } + console.log(result); + } + void callBackend(); + }; + return ; +} +``` + +Or handle as a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise): + +src/App.tsx + +TS + +``` +export function App() { + const doSomething = useMutation(api.functions.doSomething); + const onClick = () => { + doSomething() + .catch((error) => { + console.error(error); + }) + .then((result) => { + console.log(result); + }); + }; + return ; +} +``` + +Learn more about [Error Handling](/functions/error-handling/.md) in functions. + +### Retries[​](#retries "Direct link to Retries") + +Convex React automatically retries mutations until they are confirmed to have been written to the database. The Convex backend ensures that despite multiple retries, every mutation call only executes once. + +Additionally, Convex React will warn users if they try to close their browser tab while there are outstanding mutations. This means that when you call a Convex mutation, you can be sure that the user's edits won't be lost. + +### Optimistic updates[​](#optimistic-updates "Direct link to Optimistic updates") + +Convex queries are fully reactive, so all query results will be automatically updated after a mutation. Sometimes you may want to update the UI before the mutation changes propagate back to the client. To accomplish this, you can configure an *optimistic update* to execute as part of your mutation. + +Optimistic updates are temporary, local changes to your query results which are used to make your app more responsive. + +See [Optimistic Updates](/client/react/optimistic-updates.md) on how to configure them. + +## Calling third-party APIs[​](#calling-third-party-apis "Direct link to Calling third-party APIs") + +Your React app can read data, call third-party services, and write data with a single backend call using the [`useAction`](/api/modules/react.md#useaction) React hook by calling your [actions](/functions/actions.md). + +Like `useQuery` and `useMutation`, this hook is used with the `api` object generated for you in the `convex/_generated/api.js` module to provide better autocompletion in JavaScript and end-to-end type safety in [TypeScript](/understanding/best-practices/typescript.md): + +src/App.tsx + +TS + +``` +import { useAction } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const doSomeAction = useAction(api.functions.doSomeAction); + return ; +} +``` + +The hook returns an `async` function which performs the call to the action. + +### Action arguments[​](#action-arguments "Direct link to Action arguments") + +Action arguments work exactly the same as [mutation arguments](#mutation-arguments). + +### Action response and error handling[​](#action-response-and-error-handling "Direct link to Action response and error handling") + +Action response and error handling work exactly the same as [mutation response and error handling](#mutation-response-and-error-handling). + +Actions do not support automatic retries or optimistic updates. + +## Under the hood[​](#under-the-hood "Direct link to Under the hood") + +The [`ConvexReactClient`](/api/classes/react.ConvexReactClient.md) connects to your Convex deployment by creating a [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). The WebSocket provides a 2-way communication channel over TCP. This allows Convex to push new query results reactively to the client without the client needing to poll for updates. + +If the internet connection drops, the client will handle reconnecting and re-establishing the Convex session automatically. diff --git a/.claude/docs/references/convex/client/tanstack-query.mdx b/.claude/docs/references/convex/client/tanstack-query.mdx new file mode 100644 index 00000000..a52b1c14 --- /dev/null +++ b/.claude/docs/references/convex/client/tanstack-query.mdx @@ -0,0 +1,304 @@ +# Convex with TanStack Query + +[TanStack Query](https://tanstack.com/query/latest) is an excellent, popular library for managing requests to a server. + +The [`@convex-dev/react-query`](https://www.npmjs.com/package/@convex-dev/react-query) library provides [Query Option](https://tanstack.com/query/latest/docs/framework/react/guides/query-options) functions for use with TanStack Query. + +Not all features of the standard [Convex React client](/client/react.md) are available through the TanStack Query APIs but you can use the two alongside each other, dropping into the standard Convex React hooks as necessary. + +The TanStack Query adapter is in beta + +The TanStack Query adapter is currently a [beta feature](/production/state/.md#beta-features). If you have feedback or feature requests, [let us know on Discord](https://convex.dev/community)! + +This makes subscribing to a Convex query function using the TanStack Query `useQuery` hook look like this: + +``` +const { data, isPending, error } = useQuery(convexQuery(api.messages.list, {})); +``` + +Instead of the typical polling pattern for API endpoints used with TanStack Query, the code above receives updates for this `api.messages.list` query from the Convex server reactively. New results for all relevant subscriptions are pushed to the client where they update at the same time so data is never stale and there's no need to manually invalidate queries. + +Support for other frameworks + +Currently only [React Query](https://tanstack.com/query/latest/docs/framework/react/overview) is supported via [`@convex-dev/react-query`](https://www.npmjs.com/package/@convex-dev/react-query). [Let us know](https://convex.dev/community) if you would find support for vue-query, svelte-query, solid-query, or angular-query helpful. + +## Setup[​](#setup "Direct link to Setup") + +To get live updates in TanStack Query create a `ConvexQueryClient` and connect it to the TanStack Query [QueryClient](https://tanstack.com/query/latest/docs/reference/QueryClient). After installing the adapter library with + +``` +npm i @convex-dev/react-query +``` + +wire up Convex to TanStack Query like this: + +src/main.tsx + +``` +import { ConvexQueryClient } from "@convex-dev/react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ConvexProvider, ConvexReactClient } from "convex/react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); +const convexQueryClient = new ConvexQueryClient(convex); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + queryKeyHashFn: convexQueryClient.hashFn(), + queryFn: convexQueryClient.queryFn(), + }, + }, +}); +convexQueryClient.connect(queryClient); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + , +); +``` + +Note that when your create your React tree you should both: + +* wrap your app in the TanStack Query [`QueryClientProvider`](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider) so you can use [TanStack Query hooks](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) and +* wrap your app in the [`ConvexProvider`](/api/modules/react.md#convexprovider) so you can also use normal [Convex React](/client/react.md) hooks + +## Queries[​](#queries "Direct link to Queries") + +A live-updating subscription to a Convex [query](/functions/query-functions.md) is as simple as calling TanStack [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) with `convexQuery`: + +``` +import { useQuery } from "@tanstack/react-query"; +import { convexQuery } from "@convex-dev/react-query"; +import { api } from "../convex/_generated/api"; + +export function App() { + const { data, isPending, error } = useQuery( + convexQuery(api.functions.myQuery, { id: 123 }), + ); + return isPending ? "Loading..." : data; +} +``` + +You can spread the object returned by `convexQuery` into an object specifying additional [arguments of `useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery). + +``` +const { data, isPending, error } = useQuery({ + ...convexQuery(api.functions.myQuery, { id: 123 }), + initialData: [], // use an empty list if no data is available yet + gcTime: 10000, // stay subscribed for 10 seconds after this component unmounts +}); +``` + +## Mutations[​](#mutations "Direct link to Mutations") + +Your app can call Convex [mutations](/functions/mutation-functions.md) by using the TanStack [`useMutation`](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) hook, and setting the `mutationFn` property to the result of calling `useConvexMutation`: + +``` +import { useMutation } from "@tanstack/react-query"; +import { useConvexMutation } from "@convex-dev/react-query"; +import { api } from "../convex/_generated/api"; + +export function App() { + const { mutate, isPending } = useMutation({ + mutationFn: useConvexMutation(api.functions.doSomething), + }); + return ; +} +``` + +`useConvexMutation` is just a re-export of the [`useMutation`](/client/react.md#editing-data) hook from [Convex React](/client/react.md). + +## Differences from using `fetch` with TanStack Query[​](#differences-from-using-fetch-with-tanstack-query "Direct link to differences-from-using-fetch-with-tanstack-query") + +Convex provides stronger guarantees than other methods of fetching data with React Query, so some options and return value properties are no longer necessary. + +Subscriptions to Convex queries will remain active after the last component using `useQuery` for a given function unmounts for `gcTime` milliseconds. This value is 5 minutes by default; if this results in unwanted function activity use a smaller value. + +Data provided by Convex is never stale, so the `isStale` property of the return value of `useQuery` will always be false. `retry`-related options are ignored, since Convex provides its own retry mechanism over its WebSocket protocol. `refetch`-related options are similarly ignored since Convex queries are always up to date. + +# @convex-dev/react-query + +Instead of polling you subscribe to receive update from server-side query +functions in a Convex deployment. Convex is a database with server-side +(db-side? like stored procedures) functions that update reactively. + +New results for all relevant subscriptions are pushed to the client where they +update at the same time so data is never stale and there's no need to call +`queryClient.invalidateQueries()`. + +## Setup + +See [./src/example.tsx](./src/example.tsx) for a real example. The general +pattern: + +1. Create a ConvexClient and ConvexQueryClient. Set the global default + `queryKeyHashFn` to `convexQueryClient.hashFn()` and `queryFn` to + `convexQueryClient.queryFn()`. Connect the ConvexQueryClient to the React + Query QueryClient. + +```ts +const convexClient = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); +const convexQueryClient = new ConvexQueryClient(convexClient); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + queryKeyHashFn: convexQueryClient.hashFn(), + queryFn: convexQueryClient.queryFn(), + }, + }, +}); +convexQueryClient.connect(queryClient); +``` + +2. Use `useQuery()` with the `convexQuery` options factory function called with + an `api` object imported from `../convex/_generated/server` and the arguments + for this query function. These two form the query key. + +```ts +const { isPending, error, data } = useQuery({ + ...convexQuery(api.repos.get, { repo: "made/up" }), + gcTime: 10000, // unsubscribe after 10s of no use +}); +``` + +`staleTime` is set to `Infinity` beacuse this data is never stale; it's +proactively updated whenever the query result updates on the server. (see +[tkdodo's post](https://tkdodo.eu/blog/using-web-sockets-with-react-query#increasing-staletime) +for more about this) If you like, customize the `gcTime` to the length of time a +query subscription should remain active after all `useQuery()` hooks using it +have unmounted. + +If you need to use a Convex Action as a query, it won't be reactive; you'll get +all the normal tools from React Query to refetch it. + +# Differences from using TanStack Query with `fetch` + +New query results are pushed from the server, so a `staleTime` of `Infinity` +should be used. + +Your app will remain subscribed to a query until the `gcTime` has elapsed. Tune +this for your app: it's usually a good tradeoff to use a value of at least a +couple seconds. + +# Example + +To run this example: + +- `npm install` +- `npm run dev` + +# Mutations and Actions + +If you wrap your app in a `ConvexProvider` you'll be able to use convex hooks +like `useConvexMutation` and `useConvexAction`.: + +```tsx + + + + + +``` + +You can use this mutation function directly or wrap it in a TanStack Query +`useMutation`: + +```ts +const mutationFn = useConvexMutation(api.board.createColumn); +const { mutate } = useMutation({ mutationFn }); +``` + +```ts +const { mutate } = useMutation({ + mutationFn: useConvexAction(api.time.getTotal), +}); +``` + +# Authentication + +**Note:** The example app includes a basic Convex Auth implementation for +reference. + +TanStack Query isn't opionated about auth; an auth code might be a an element of +a query key like any other. With Convex it's not necessary to add an additional +key for an auth code; auth is an implicit argument to all Convex queries and +these queries will be retried when authentication info changes. + +Convex auth is typically done via JWT: some query functions will fail if +requested before calling `convexReactClinet.setAuth()` with a function that +provides the token. + +Auth setup looks just like it's recommended in +[Convex docs](https://docs.convex.dev/auth), which make use of components that +use native convex hooks. For Clerk, this might look like this: a `ClerkProvider` +for auth, a `ConvexProviderWithClerk` for the convex client, and a +`QueryClient`. + +``` + + + + + + + +``` + +See the [Convex Auth docs](https://docs.convex.dev/auth) for setup instructions. + +# TODO + +- auth +- paginated queries +- cleanup / unsubscribe in useEffect; something with hot reloading may not be + working right + +Query Caching +Utilize a query cache implementation which persists subscriptions to the server for some expiration period even after app useQuery hooks have all unmounted. This allows very fast reloading of unevicted values during navigation changes, view changes, etc. + +Note: unlike other forms of caching, subscription caching will mean strictly more bandwidth usage, because it will keep the subscription open even after the component unmounts. This is for optimizing the user experience, not database bandwidth. + +Related files: + +cache.ts re-exports things so you can import from a single convenient location. +provider.tsx contains ConvexQueryCacheProvider, a configurable cache provider you put in your react app's root. +hooks.ts contains cache-enabled drop-in replacements for useQuery, usePaginatedQuery, and useQueries. +To use the cache, first make sure to put a inside in your react component tree: + +import { ConvexQueryCacheProvider } from "convex-helpers/react/cache"; +// For Next.js, import from "convex-helpers/react/cache/provider"; instead + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} +This provider takes three optional props: + +expiration (number) -- Milliseconds to preserve unmounted subscriptions in the cache. After this, the subscriptions will be dropped, and the value will have to be re-fetched from the server. (Default: 300000, aka 5 minutes) +maxIdleEntires (number) -- Maximum number of unused subscriptions kept in the cache. (Default: 250). +debug (boolean) -- Dump console logs every 3s to debug the state of the cache (Default: false). +Finally, you can utilize useQuery (and useQueries) just the same as their convex/react equivalents. + +import { useQuery } from "convex-helpers/react/cache"; +// For Next.js, import from "convex-helpers/react/cache/hooks"; instead + +// ... + +const users = useQuery(api.todos.getAll); diff --git a/.claude/docs/references/convex/dev-workflow.mdx b/.claude/docs/references/convex/dev-workflow.mdx new file mode 100644 index 00000000..2b38d3f2 --- /dev/null +++ b/.claude/docs/references/convex/dev-workflow.mdx @@ -0,0 +1,151 @@ +# Dev workflow + +Let's walk through everything that needs to happen from creating a new project to launching your app in production. + +This doc assumes you are building an app with Convex and React and you already have a basic React app already up and running. You can follow one of our [quickstarts](/quickstarts) to set this up. + +## Installing and running Convex[​](#installing-and-running-convex "Direct link to Installing and running Convex") + +You install Convex adding the npm dependency to your app: + +``` +npm i convex +``` + +Then you create your Convex project and start the backend dev loop: + +``` +npx convex dev +``` + +The first time you run the `npx convex dev` command you'll be asked whether you want start developing locally without an account or create an account. + +### Developing without an account[​](#developing-without-an-account "Direct link to Developing without an account") + +`npx convex dev` will prompt you for the name of your project, and then start running the open-source Convex backend locally on your machine (this is also called a "deployment"). + +The data for your project will be saved in the `~/.convex` directory. + +1. The name of your project will get saved to your `.env.local` file so future runs of `npx convex dev` will know to use this project. +2. A `convex/` folder will be created (if it doesn't exist), where you'll write your Convex backend functions. + +You can run `npx convex login` in the future to create an account and link any existing projects. + +### Developing with an account[​](#developing-with-an-account "Direct link to Developing with an account") + +`npx convex dev` will prompt you through creating an account if one doesn't exist, and will add your credentials to `~/.convex/config.json` on your machine. You can run `npx convex logout` to log you machine out of the account in the future. + +Next, `npx convex dev` will create a new project and provision a new personal development deployment for this project: + +1. Deployment details will automatically be added to your `.env.local` file so future runs of `npx convex dev` will know which dev deployment to connect to. +2. A `convex/` folder will be created (if it doesn't exist), where you'll write your Convex backend functions. + +![Convex directory in your app](/assets/images/convex-directory-1ede9882007bf42d249b0561f2892c54.png) + +## Running the dev loop[​](#running-the-dev-loop "Direct link to Running the dev loop") + +Keep the `npx convex dev` command running while you're working on your Convex app. This continuously pushes backend code you write in the `convex/` folder to your deployment. It also keeps the necessary TypeScript types up-to-date as you write your backend code. + +When you're developing with a locally running deployment, `npx convex dev` is also responsible for running your deployment. + +You can then add new server functions to your Convex backend: + +convex/tasks.ts + +``` +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +// Return the last 100 tasks in a given task list. +export const getTaskList = query({ + args: { taskListId: v.id("taskLists") }, + handler: async (ctx, args) => { + const tasks = await ctx.db + .query("tasks") + .withIndex("taskListId", (q) => q.eq("taskListId", args.taskListId)) + .order("desc") + .take(100); + return tasks; + }, +}); +``` + +When you write and save this code in your editor, several things happen: + +1. The `npx convex dev` command typechecks your code and updates the `convex/_generated` directory. +2. The contents of your `convex/` directory get uploaded to your dev deployment. +3. Your Convex dev deployment analyzes your code and finds all Convex functions. In this example, it determines that `tasks.getTaskList` is a new public query function. +4. If there are any changes to the [schema](/database/schemas.md), the deployment will automatically enforce them. +5. The `npx convex dev` command updates generated TypeScript code in the `convex/_generated` directory to provide end to end type safety for your functions. + +tip + +Check in everything in your `convex/_generated/` directory. This it ensures that your code immediately type checks and runs without having to first run `npx convex dev`. It's particularly useful when non-backend developers are writing frontend code and want to ensure their code type checks against currently deployed backend code. + +Once this is done you can use your new server function in your frontend: + +src/App.tsx + +``` +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const data = useQuery(api.tasks.getTaskList); + return data ?? "Loading..."; +} +``` + +If you have other configuration like [crons](/scheduling/cron-jobs.md) or [auth](/auth.md) in your `convex/` folder, Convex ensures that they are applied and enforced on your backend. + +## Convex dashboard[​](#convex-dashboard "Direct link to Convex dashboard") + +The [Convex dashboard](/dashboard/deployments/.md) will be a trusty helper throughout your dev, debug and deploy workflow in Convex. + +`npx convex dashboard` will open a link to the dashboard for your deployment. + +### Logs[​](#logs "Direct link to Logs") + +Since Convex functions are TypeScript functions you can always use the standard `console.log` and `console.time` functions to debug your apps. + +Logs from your functions show up [in your dashboard](/dashboard/deployments/logs.md). + +![Logs Dashboard Page](/assets/images/logs-ed208103a42edfb005e9089a8edad58e.png) + +### Health, Data, Functions and more[​](#health-data-functions-and-more "Direct link to Health, Data, Functions and more") + +* [Health](/dashboard/deployments/health.md) - provides invaluable information on how your app is performing in production, with deep insights on how your Convex queries are doing. +* [Data](/dashboard/deployments/data.md) - gives you a complete data browser to spot check your data. +* [Functions](/dashboard/deployments/functions.md) - gives you stats and run functions to debug them. + +There is a lot more to to the dashboard. Be sure to click around or [check out the docs](/dashboard.md). + +## Deploying your app[​](#deploying-your-app "Direct link to Deploying your app") + +So far you've been working on your app against your personal dev deployment. + +All Convex projects have one production deployment running in the cloud. It has separate data and has a separate push process from personal dev deployments, which allows you and your teammates to work on new features using personal dev deployments without disrupting your app running in production. + +If you have not created a Convex account yet, you will need to do so with `npx convex login`. This will automatically link any projects you've started with your new account, and enable using your production deployment. + +To push your code to your production deployment for your project you run the deploy command: + +``` +npx convex deploy +``` + +info + +If you're running this command for the first time, it will automatically provision the prod deployment for your project. + +### Setting up your deployment pipeline[​](#setting-up-your-deployment-pipeline "Direct link to Setting up your deployment pipeline") + +It's rare to run `npx convex deploy` directly. Most production applications run an automated workflow that runs tests and deploys your backend and frontend together. + +You can see detailed deployment and frontend configuration instructions in the [Hosting and Deployment](/production/hosting/.md) doc. For most React meta-frameworks Convex [automatically sets the correct environment variable](/production/hosting/vercel.md#how-it-works) to connect to the production deployment. + +## Up next[​](#up-next "Direct link to Up next") + +You now know the basics of how Convex works and fits in your app. Go head and explore the docs further to learn more about the specific features you want to use. + +Whenever you're ready be sure the read the [Best Practices](/understanding/best-practices/.md), and then the [Zen of Convex](/understanding/zen.md) once you are ready to "think in Convex." diff --git a/.claude/docs/references/convex/functions/action.mdx b/.claude/docs/references/convex/functions/action.mdx new file mode 100644 index 00000000..030def7e --- /dev/null +++ b/.claude/docs/references/convex/functions/action.mdx @@ -0,0 +1,382 @@ +# Actions + +Actions can call third party services to do things such as processing a payment with [Stripe](https://stripe.com). They can be run in Convex's JavaScript environment or in Node.js. They can interact with the database indirectly by calling [queries](/functions/query-functions.md) and [mutations](/functions/mutation-functions.md). + +**Example:** [GIPHY Action](https://github.com/get-convex/convex-demos/tree/main/giphy-action) + +## Action names[​](#action-names "Direct link to Action names") + +Actions follow the same naming rules as queries, see [Query names](/functions/query-functions.md#query-names). + +## The `action` constructor[​](#the-action-constructor "Direct link to the-action-constructor") + +To declare an action in Convex you use the action constructor function. Pass it an object with a `handler` function, which performs the action: + +convex/myFunctions.ts + +TS + +``` +import { action } from "./_generated/server"; + +export const doSomething = action({ + args: {}, + handler: () => { + // implementation goes here + + // optionally return a value + return "success"; + }, +}); +``` + +Unlike a query, an action can but does not have to return a value. + +### Action arguments and responses[​](#action-arguments-and-responses "Direct link to Action arguments and responses") + +Action arguments and responses follow the same rules as [mutations](/functions/mutation-functions.md#mutation-arguments): + +convex/myFunctions.ts + +TS + +``` +import { action } from "./_generated/server"; +import { v } from "convex/values"; + +export const doSomething = action({ + args: { a: v.number(), b: v.number() }, + handler: (_, args) => { + // do something with `args.a` and `args.b` + + // optionally return a value + return "success"; + }, +}); +``` + +The first argument to the handler function is reserved for the action context. + +### Action context[​](#action-context "Direct link to Action context") + +The `action` constructor enables interacting with the database, and other Convex features by passing an [ActionCtx](/api/interfaces/server.GenericActionCtx.md) object to the handler function as the first argument: + +convex/myFunctions.ts + +TS + +``` +import { action } from "./_generated/server"; +import { v } from "convex/values"; + +export const doSomething = action({ + args: { a: v.number(), b: v.number() }, + handler: (ctx, args) => { + // do something with `ctx` + }, +}); +``` + +Which part of that action context is used depends on what your action needs to do: + +* To read data from the database use the `runQuery` field, and call a query that performs the read: + + convex/myFunctions.ts + + TS + + ``` + import { action, internalQuery } from "./_generated/server"; + import { internal } from "./_generated/api"; + import { v } from "convex/values"; + + export const doSomething = action({ + args: { a: v.number() }, + handler: async (ctx, args) => { + const data = await ctx.runQuery(internal.myFunctions.readData, { + a: args.a, + }); + // do something with `data` + }, + }); + + export const readData = internalQuery({ + args: { a: v.number() }, + handler: async (ctx, args) => { + // read from `ctx.db` here + }, + }); + ``` + + Here `readData` is an [internal query](/functions/internal-functions.md) because we don't want to expose it to the client directly. Actions, mutations and queries can be defined in the same file. + +* To write data to the database use the `runMutation` field, and call a mutation that performs the write: + + convex/myFunctions.ts + + TS + + ``` + import { v } from "convex/values"; + import { action } from "./_generated/server"; + import { internal } from "./_generated/api"; + + export const doSomething = action({ + args: { a: v.number() }, + handler: async (ctx, args) => { + const data = await ctx.runMutation(internal.myMutations.writeData, { + a: args.a, + }); + // do something else, optionally use `data` + }, + }); + ``` + + Use an [internal mutation](/functions/internal-functions.md) when you want to prevent users from calling the mutation directly. + + As with queries, it's often convenient to define actions and mutations in the same file. + +* To generate upload URLs for storing files use the `storage` field. Read on about [File Storage](/file-storage.md). + +* To check user authentication use the `auth` field. Auth is propagated automatically when calling queries and mutations from the action. Read on about [Authentication](/auth.md). + +* To schedule functions to run in the future, use the `scheduler` field. Read on about [Scheduled Functions](/scheduling/scheduled-functions.md). + +* To search a vector index, use the `vectorSearch` field. Read on about [Vector Search](/search/vector-search.md). + +### Dealing with circular type inference[​](#dealing-with-circular-type-inference "Direct link to Dealing with circular type inference") + +Working around the TypeScript error: some action `implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.` + +When the return value of an action depends on the result of calling `ctx.runQuery` or `ctx.runMutation`, TypeScript will complain that it cannot infer the return type of the action. This is a minimal example of the issue: + +convex/myFunctions.ts + +``` +// TypeScript reports an error on `myAction` +export const myAction = action({ + args: {}, + handler: async (ctx) => { + return await ctx.runQuery(api.myFunctions.getSomething); + }, +}); + +export const getSomething = query({ + args: {}, + handler: () => { + return null; + }, +}); +``` + +To work around this, there are two options: + +1. Type the return value of the handler function explicitly: + + + + convex/myFunctions.ts + + ``` + export const myAction = action({ + args: {}, + handler: async (ctx): Promise => { + const result = await ctx.runQuery(api.myFunctions.getSomething); + return result; + }, + }); + ``` + +2. Type the the result of the `ctx.runQuery` or `ctx.runMutation` call explicitly: + + + + convex/myFunctions.ts + + ``` + export const myAction = action({ + args: {}, + handler: async (ctx) => { + const result: null = await ctx.runQuery(api.myFunctions.getSomething); + return result; + }, + }); + ``` + +TypeScript will check that the type annotation matches what the called query or mutation returns, so you don't lose any type safety. + +In this trivial example the return type of the query was `null`. See the [TypeScript](/understanding/best-practices/typescript.md#type-annotating-server-side-helpers) page for other types which might be helpful when annotating the result. + +## Choosing the runtime ("use node")[​](#choosing-the-runtime-use-node "Direct link to Choosing the runtime (\"use node\")") + +Actions can run in Convex's custom JavaScript environment or in Node.js. + +By default, actions run in Convex's environment. This environment supports `fetch`, so actions that simply want to call a third-party API using `fetch` can be run in this environment: + +convex/myFunctions.ts + +TS + +``` +import { action } from "./_generated/server"; + +export const doSomething = action({ + args: {}, + handler: async () => { + const data = await fetch("https://api.thirdpartyservice.com"); + // do something with data + }, +}); +``` + +Actions running in Convex's environment are faster compared to Node.js, since they don't require extra time to start up before running your action (cold starts). They can also be defined in the same file as other Convex functions. Like queries and mutations they can import NPM packages, but not all are supported. + +Actions needing unsupported NPM packages or Node.js APIs can be configured to run in Node.js by adding the `"use node"` directive at the top of the file. Note that other Convex functions cannot be defined in files with the `"use node";` directive. + +convex/myAction.ts + +TS + +``` +"use node"; + +import { action } from "./_generated/server"; +import SomeNpmPackage from "some-npm-package"; + +export const doSomething = action({ + args: {}, + handler: () => { + // do something with SomeNpmPackage + }, +}); +``` + +Learn more about the two [Convex Runtimes](/functions/runtimes.md). + +## Splitting up action code via helpers[​](#splitting-up-action-code-via-helpers "Direct link to Splitting up action code via helpers") + +Just like with [queries](/functions/query-functions.md#splitting-up-query-code-via-helpers) and [mutations](/functions/mutation-functions.md#splitting-up-mutation-code-via-helpers) you can define and call helper + +TypeScript + +functions to split up the code in your actions or reuse logic across multiple Convex functions. + +But note that the [ActionCtx](/api/interfaces/server.GenericActionCtx.md) only has the `auth` field in common with [QueryCtx](/generated-api/server.md#queryctx) and [MutationCtx](/generated-api/server.md#mutationctx). + +## Calling actions from clients[​](#calling-actions-from-clients "Direct link to Calling actions from clients") + +To call an action from [React](/client/react.md) use the [`useAction`](/api/modules/react.md#useaction) hook along with the generated [`api`](/generated-api/api.md) object. + +src/myApp.tsx + +TS + +``` +import { useAction } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function MyApp() { + const performMyAction = useAction(api.myFunctions.doSomething); + const handleClick = () => { + performMyAction({ a: 1 }); + }; + // pass `handleClick` to a button + // ... +} +``` + +Unlike [mutations](/functions/mutation-functions.md#calling-mutations-from-clients), actions from a single client are parallelized. Each action will be executed as soon as it reaches the server (even if other actions and mutations from the same client are running). If your app relies on actions running after other actions or mutations, make sure to only trigger the action after the relevant previous function completes. + +**Note:** In most cases calling an action directly from a client **is an anti-pattern**. Instead, have the client call a [mutation](/functions/mutation-functions.md) which captures the user intent by writing into the database and then [schedules](/scheduling/scheduled-functions.md) an action: + +convex/myFunctions.ts + +TS + +``` +import { v } from "convex/values"; +import { internal } from "./_generated/api"; +import { internalAction, mutation } from "./_generated/server"; + +export const mutationThatSchedulesAction = mutation({ + args: { text: v.string() }, + handler: async (ctx, { text }) => { + const taskId = await ctx.db.insert("tasks", { text }); + await ctx.scheduler.runAfter(0, internal.myFunctions.actionThatCallsAPI, { + taskId, + text, + }); + }, +}); + +export const actionThatCallsAPI = internalAction({ + args: { taskId: v.id("tasks"), text: v.string() }, + handler: (_, args): void => { + // do something with `taskId` and `text`, like call an API + // then run another mutation to store the result + }, +}); +``` + +This way the mutation can enforce invariants, such as preventing the user from executing the same action twice. + +## Limits[​](#limits "Direct link to Limits") + +Actions time out after 10 minutes. [Node.js](/functions/runtimes.md#nodejs-runtime) and [Convex runtime](/functions/runtimes.md#default-convex-runtime) have 512MB and 64MB memory limit respectively. Please [contact us](/production/contact.md) if you have a use case that requires configuring higher limits. + +Actions can do up to 1000 concurrent operations, such as executing queries, mutations or performing fetch requests. + +For information on other limits, see [here](/production/state/limits.md). + +## Error handling[​](#error-handling "Direct link to Error handling") + +Unlike queries and mutations, actions may have side-effects and therefore can't be automatically retried by Convex when errors occur. For example, say your action calls Stripe to send a customer invoice. If the HTTP request fails, Convex has no way of knowing if the invoice was already sent. Like in normal backend code, it is the responsibility of the caller to handle errors raised by actions and retry the action call if appropriate. + +## Dangling promises[​](#dangling-promises "Direct link to Dangling promises") + +Make sure to await all promises created within an action. Async tasks still running when the function returns might or might not complete. In addition, since the Node.js execution environment might be reused between action calls, dangling promises might result in errors in subsequent action invocations. + +## Best practices[​](#best-practices "Direct link to Best practices") + +### `await ctx.runAction` should only be used for crossing JS runtimes[​](#await-ctxrunaction-should-only-be-used-for-crossing-js-runtimes "Direct link to await-ctxrunaction-should-only-be-used-for-crossing-js-runtimes") + +**Why?** `await ctx.runAction` incurs to overhead of another Convex server function. It counts as an extra function call, it allocates its own system resources, and while you're awaiting this call the parent action call is frozen holding all it's resources. If you pile enough of these calls on top of each other, your app may slow down significantly. + +**Fix:** The reason this api exists is to let you run code in the [Node.js environment](/functions/runtimes.md). If you want to call an action from another action that's in the same runtime, which is the normal case, the best way to do this is to pull the code you want to call into a TypeScript [helper function](/understanding/best-practices/.md#use-helper-functions-to-write-shared-code) and call the helper instead. + +### Avoid `await ctx.runMutation` / `await ctx.runQuery`[​](#avoid-await-ctxrunmutation--await-ctxrunquery "Direct link to avoid-await-ctxrunmutation--await-ctxrunquery") + +``` +// ❌ +const foo = await ctx.runQuery(...) +const bar = await ctx.runQuery(...) + +// ✅ +const fooAndBar = await ctx.runQuery(...) +``` + +**Why?** Multiple runQuery / runMutations execute in separate transactions and aren’t guaranteed to be consistent with each other (e.g. foo and bar could read the same document and return two different results), while a single runQuery / runMutation will always be consistent. Additionally, you’re paying for multiple function calls when you don’t have to. + +**Fix:** Make a new internal query / mutation that does both things. Refactoring the code for the two functions into helpers will make it easy to create a new internal function that does both things while still keeping around the original functions. Potentially try and refactor your action code to “batch” all the database access. + +Caveats: Separate runQuery / runMutation calls are valid when intentionally trying to process more data than fits in a single transaction (e.g. running a migration, doing a live aggregate). + +## Related Components[​](#related-components "Direct link to Related Components") + +[Convex Component](https://www.convex.dev/components/action-cache) + +### [Action Cache](https://www.convex.dev/components/action-cache) + +[Cache expensive or frequently run actions. Allows configurable cache duration and forcing updates.](https://www.convex.dev/components/action-cache) + +[Convex Component](https://www.convex.dev/components/workpool) + +### [Workpool](https://www.convex.dev/components/workpool) + +[Workpool give critical tasks priority by organizing async operations into separate, customizable queues. Supports retries and parallelism limits.](https://www.convex.dev/components/workpool) + +[Convex Component](https://www.convex.dev/components/workflow) + +### [Workflow](https://www.convex.dev/components/workflow) + +[Similar to Actions, Workflows can call queries, mutations, and actions. However, they are durable functions that can suspend, survive server crashes, specify retries for action calls, and more.](https://www.convex.dev/components/workflow) diff --git a/.claude/docs/references/convex/functions/error-handling.mdx b/.claude/docs/references/convex/functions/error-handling.mdx new file mode 100644 index 00000000..8a01079a --- /dev/null +++ b/.claude/docs/references/convex/functions/error-handling.mdx @@ -0,0 +1,235 @@ +# Error Handling + +There are four reasons why your Convex [queries](/functions/query-functions.md) and [mutations](/functions/mutation-functions.md) may hit errors: + +1. [Application Errors](#application-errors-expected-failures): The function code hits a logical condition that should stop further processing, and your code throws a `ConvexError` +2. Developer Errors: There is a bug in the function (like calling `db.get("documents", null)` instead of `db.get("documents", id)`). +3. [Read/Write Limit Errors](#readwrite-limit-errors): The function is retrieving or writing too much data. +4. Internal Convex Errors: There is a problem within Convex (like a network blip). + +Convex will automatically handle internal Convex errors. If there are problems on our end, we'll automatically retry your queries and mutations until the problem is resolved and your queries and mutations succeed. + +On the other hand, you must decide how to handle application, developer and read/write limit errors. When one of these errors happens, the best practices are to: + +1. Show the user some appropriate UI. +2. Send the error to an exception reporting service using the [Exception Reporting Integration](/production/integrations/exception-reporting.md). +3. Log the incident using `console.*` and set up reporting with [Log Streaming](/production/integrations/log-streams/.md). This can be done in addition to the above options, and doesn't require an exception to be thrown. + +Additionally, you might also want to send client-side errors to a service like [Sentry](https://sentry.io) to capture additional information for debugging and observability. + +## Errors in queries[​](#errors-in-queries "Direct link to Errors in queries") + +If your query function hits an error, the error will be sent to the client and thrown from your `useQuery` call site. **The best way to handle these errors is with a React [error boundary component](https://reactjs.org/docs/error-boundaries.html).** + +Error boundaries allow you to catch errors thrown in their child component tree, render fallback UI, and send information about the error to your exception handling service. Adding error boundaries to your app is a great way to handle errors in Convex query functions as well as other errors in your React components. If you are using Sentry, you can use their [`Sentry.ErrorBoundary`](https://docs.sentry.io/platforms/javascript/guides/react/components/errorboundary/) component. + +With error boundaries, you can decide how granular you'd like your fallback UI to be. One simple option is to wrap your entire application in a single error boundary like: + +``` + + + + + + +, +``` + +Then any error in any of your components will be caught by the boundary and render the same fallback UI. + +On the other hand, if you'd like to enable some portions of your app to continue functioning even if other parts hit errors, you can instead wrap different parts of your app in separate error boundaries. + +Retrying + +Unlike other frameworks, there is no concept of "retrying" if your query function hits an error. Because Convex functions are [deterministic](/functions/query-functions.md#caching--reactivity--consistency), if the query function hits an error, retrying will always produce the same error. There is no point in running the query function with the same arguments again. + +## Errors in mutations[​](#errors-in-mutations "Direct link to Errors in mutations") + +If a mutation hits an error, this will + +1. Cause the promise returned from your mutation call to be rejected. +2. Cause your [optimistic update](/client/react/optimistic-updates.md) to be rolled back. + +If you have an exception service like [Sentry](https://sentry.io/) configured, it should report "unhandled promise rejections" like this automatically. That means that with no additional work your mutation errors should be reported. + +Note that errors in mutations won't be caught by your error boundaries because the error doesn't happen as part of rendering your components. + +If you would like to render UI specifically in response to a mutation failure, you can use `.catch` on your mutation call. For example: + +``` +sendMessage(newMessageText).catch((error) => { + // Do something with `error` here +}); +``` + +If you're using an `async` handled function you can also use `try...catch`: + +``` +try { + await sendMessage(newMessageText); +} catch (error) { + // Do something with `error` here +} +``` + +Reporting caught errors + +If you handle your mutation error, it will no longer become an unhandled promise rejection. You may need to report this error to your exception handling service manually. + +## Errors in action functions[​](#errors-in-action-functions "Direct link to Errors in action functions") + +Unlike queries and mutations, [actions](//docs/functions/actions.mdx) may have side-effects and therefore can't be automatically retried by Convex when errors occur. For example, say your action sends a email. If it fails part-way through, Convex has no way of knowing if the email was already sent and can't safely retry the action. It is responsibility of the caller to handle errors raised by actions and retry if appropriate. + +## Differences in error reporting between dev and prod[​](#differences-in-error-reporting-between-dev-and-prod "Direct link to Differences in error reporting between dev and prod") + +Using a dev deployment any server error thrown on the client will include the original error message and a server-side stack trace to ease debugging. + +Using a production deployment any server error will be redacted to only include the name of the function and a generic `"Server Error"` message, with no stack trace. Server [application errors](/functions/error-handling/application-errors.md) will still include their custom `data`. + +Both development and production deployments log full errors with stack traces which can be found on the [Logs](/dashboard/deployments/logs.md) page of a given deployment. + +## Application errors, expected failures[​](#application-errors-expected-failures "Direct link to Application errors, expected failures") + +If you have expected ways your functions might fail, you can either return different values or throw `ConvexError`s. + +See [Application Errors](/functions/error-handling/application-errors.md). + +## Read/write limit errors[​](#readwrite-limit-errors "Direct link to Read/write limit errors") + +To ensure uptime and guarantee performance, Convex will catch queries and mutations that try to read or write too much data. These limits are enforced at the level of a single query or mutation function execution. The exact limits are listed in [Limits](/production/state/limits.md#transactions). + +Documents are "scanned" by the database to figure out which documents should be returned from `db.query`. So for example `db.query("table").take(5).collect()` will only need to scan 5 documents, but `db.query("table").filter(...).first()` might scan up to as many documents as there are in `"table"`, to find the first one that matches the given filter. + +The number of calls to `db.get` and `db.query` has a limit to prevent a single query from subscribing to too many index ranges, or a mutation from reading from too many ranges that could cause conflicts. + +In general, if you're running into these limits frequently, we recommend [indexing your queries](/database/reading-data/indexes/.md) to reduce the number of documents scanned, allowing you to avoid unnecessary reads. Queries that scan large swaths of your data may look innocent at first, but can easily blow up at any production scale. If your functions are close to hitting these limits they will log a warning. + +For information on other limits, see [here](/production/state/limits.md). + +## Debugging Errors[​](#debugging-errors "Direct link to Debugging Errors") + +See [Debugging](/functions/debugging.md) and specifically [Finding relevant logs by Request ID](/functions/debugging.md#finding-relevant-logs-by-request-id). + +## Related Components[​](#related-components "Direct link to Related Components") + +[Convex Component](https://www.convex.dev/components/workpool) + +### [Workpool](https://www.convex.dev/components/workpool) + +[Workpool give critical tasks priority by organizing async operations into separate, customizable queues. Supports retries and parallelism limits.](https://www.convex.dev/components/workpool) + +[Convex Component](https://www.convex.dev/components/workflow) + +### [Workflow](https://www.convex.dev/components/workflow) + +[Simplify programming long running code flows. Workflows execute durably with configurable retries and delays.](https://www.convex.dev/components/workflow) + +# Application Errors + +If you have expected ways your functions might fail, you can either return different values or throw `ConvexError`s. + +## Returning different values[​](#returning-different-values "Direct link to Returning different values") + +If you're using TypeScript different return types can enforce that you're handling error scenarios. + +For example, a `createUser` mutation could return + +``` +Id<"users"> | { error: "EMAIL_ADDRESS_IN_USE" }; +``` + +to express that either the mutation succeeded or the email address was already taken. + +This ensures that you remember to handle these cases in your UI. + +## Throwing application errors[​](#throwing-application-errors "Direct link to Throwing application errors") + +You might prefer to throw errors for the following reasons: + +* You can use the exception bubbling mechanism to throw from a deeply nested function call, instead of manually propagating error results up the call stack. This will work for `runQuery`, `runMutation` and `runAction` calls in [actions](/functions/actions.md) too. +* In [mutations](/functions/mutation-functions.md), throwing an error will prevent the mutation transaction from committing +* On the client, it might be simpler to handle all kinds of errors, both expected and unexpected, uniformly + +Convex provides an error subclass, [`ConvexError`](/api/classes/values.ConvexError.md), which can be used to carry information from the backend to the client: + +convex/myFunctions.ts + +TS + +``` +import { ConvexError } from "convex/values"; +import { mutation } from "./_generated/server"; + +export const assignRole = mutation({ + args: { + // ... + }, + handler: (ctx, args) => { + const isTaken = isRoleTaken(/* ... */); + if (isTaken) { + throw new ConvexError("Role is already taken"); + } + // ... + }, +}); +``` + +### Application error `data` payload[​](#application-error-data-payload "Direct link to application-error-data-payload") + +You can pass the same [data types](/database/types.md) supported by function arguments, return types and the database, to the `ConvexError` constructor. This data will be stored on the `data` property of the error: + +``` +// error.data === "My fancy error message" +throw new ConvexError("My fancy error message"); + +// error.data === {message: "My fancy error message", code: 123, severity: "high"} +throw new ConvexError({ + message: "My fancy error message", + code: 123, + severity: "high", +}); + +// error.data === {code: 123, severity: "high"} +throw new ConvexError({ + code: 123, + severity: "high", +}); +``` + +Error payloads more complicated than a simple `string` are helpful for more structured error logging, or for handling sets of errors differently on the client. + +## Handling application errors on the client[​](#handling-application-errors-on-the-client "Direct link to Handling application errors on the client") + +On the client, application errors also use the `ConvexError` class, and the data they carry can be accessed via the `data` property: + +src/App.tsx + +TS + +``` +import { ConvexError } from "convex/values"; +import { useMutation } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function MyApp() { + const doSomething = useMutation(api.myFunctions.mutateSomething); + const handleSomething = async () => { + try { + await doSomething({ a: 1, b: 2 }); + } catch (error) { + const errorMessage = + // Check whether the error is an application error + error instanceof ConvexError + ? // Access data and cast it to the type we expect + (error.data as { message: string }).message + : // Must be some developer error, + // and prod deployments will not + // reveal any more information about it + // to the client + "Unexpected error occurred"; + // do something with `errorMessage` + } + }; + // ... +} +``` diff --git a/.claude/docs/references/convex/functions/functions.mdx b/.claude/docs/references/convex/functions/functions.mdx new file mode 100644 index 00000000..96630a01 --- /dev/null +++ b/.claude/docs/references/convex/functions/functions.mdx @@ -0,0 +1,21 @@ +# Functions + +Functions run on the backend and are written in JavaScript (or TypeScript). They are automatically available as APIs accessed through [client libraries](/client/react.md). Everything you do in the Convex backend starts from functions. + +There are three types of functions: + +* [Queries](/functions/query-functions.md) read data from your Convex database and are automatically cached and subscribable (realtime, reactive). +* [Mutations](/functions/mutation-functions.md) write data to the database and run as a transaction. +* [Actions](/functions/actions.md) can call OpenAI, Stripe, Twilio, or any other service or API you need to make your app work. + +You can also build [HTTP actions](/functions/http-actions.md) when you want to call your functions from a webhook or a custom client. + +Here's an overview of the three different types of Convex functions and what they can do: + +| | Queries | Mutations | Actions | +| -------------------------- | ------- | --------- | ------- | +| Database access | Yes | Yes | No | +| Transactional | Yes | Yes | No | +| Cached | Yes | No | No | +| Real-time Updates | Yes | No | No | +| External API Calls (fetch) | No | No | Yes | diff --git a/.claude/docs/references/convex/functions/mutation.mdx b/.claude/docs/references/convex/functions/mutation.mdx new file mode 100644 index 00000000..833fa58f --- /dev/null +++ b/.claude/docs/references/convex/functions/mutation.mdx @@ -0,0 +1,252 @@ +# Mutations + +Mutations insert, update and remove data from the database, check authentication or perform other business logic, and optionally return a response to the client application. + +This is an example mutation, taking in named arguments, writing data to the database and returning a result: + +convex/myFunctions.ts + +TS + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +// Create a new task with the given text +export const createTask = mutation({ + args: { text: v.string() }, + handler: async (ctx, args) => { + const newTaskId = await ctx.db.insert("tasks", { text: args.text }); + return newTaskId; + }, +}); +``` + +Read on to understand how to build mutations yourself. + +## Mutation names[​](#mutation-names "Direct link to Mutation names") + +Mutations follow the same naming rules as queries, see [Query names](/functions/query-functions.md#query-names). + +Queries and mutations can be defined in the same file when using named exports. + +## The `mutation` constructor[​](#the-mutation-constructor "Direct link to the-mutation-constructor") + +To declare a mutation in Convex use the `mutation` constructor function. Pass it an object with a `handler` function, which performs the mutation: + +convex/myFunctions.ts + +TS + +``` +import { mutation } from "./_generated/server"; + +export const mutateSomething = mutation({ + args: {}, + handler: () => { + // implementation will be here + }, +}); +``` + +Unlike a query, a mutation can but does not have to return a value. + +### Mutation arguments[​](#mutation-arguments "Direct link to Mutation arguments") + +Just like queries, mutations accept named arguments, and the argument values are accessible as fields of the second parameter of the `handler` function: + +convex/myFunctions.ts + +TS + +``` +import { mutation } from "./_generated/server"; + +export const mutateSomething = mutation({ + handler: (_, args: { a: number; b: number }) => { + // do something with `args.a` and `args.b` + + // optionally return a value + return "success"; + }, +}); +``` + +Arguments and responses are automatically serialized and deserialized, and you can pass and return most value-like JavaScript data to and from your mutation. + +To both declare the types of arguments and to validate them, add an `args` object using `v` validators: + +convex/myFunctions.ts + +TS + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const mutateSomething = mutation({ + args: { a: v.number(), b: v.number() }, + handler: (_, args) => { + // do something with `args.a` and `args.b` + }, +}); +``` + +See [argument validation](/functions/validation.md) for the full list of supported types and validators. + +The first parameter to the handler function is reserved for the mutation context. + +### Mutation responses[​](#mutation-responses "Direct link to Mutation responses") + +Queries can return values of any supported [Convex type](/functions/validation.md) which will be automatically serialized and deserialized. + +Mutations can also return `undefined`, which is not a valid Convex value. When a mutation returns `undefined` **it is translated to `null`** on the client. + +### Mutation context[​](#mutation-context "Direct link to Mutation context") + +The `mutation` constructor enables writing data to the database, and other Convex features by passing a [MutationCtx](/generated-api/server.md#mutationctx) object to the handler function as the first parameter: + +convex/myFunctions.ts + +TS + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const mutateSomething = mutation({ + args: { a: v.number(), b: v.number() }, + handler: (ctx, args) => { + // Do something with `ctx` + }, +}); +``` + +Which part of the mutation context is used depends on what your mutation needs to do: + +* To read from and write to the database use the `db` field. Note that we make the handler function an `async` function so we can `await` the promise returned by `db.insert()`: + + convex/myFunctions.ts + + TS + + ``` + import { mutation } from "./_generated/server"; + import { v } from "convex/values"; + + export const addItem = mutation({ + args: { text: v.string() }, + handler: async (ctx, args) => { + await ctx.db.insert("tasks", { text: args.text }); + }, + }); + ``` + + Read on about [Writing Data](/database/writing-data.md). + +* To generate upload URLs for storing files use the `storage` field. Read on about [File Storage](/file-storage.md). + +* To check user authentication use the `auth` field. Read on about [Authentication](/auth.md). + +* To schedule functions to run in the future, use the `scheduler` field. Read on about [Scheduled Functions](/scheduling/scheduled-functions.md). + +## Splitting up mutation code via helpers[​](#splitting-up-mutation-code-via-helpers "Direct link to Splitting up mutation code via helpers") + +When you want to split up the code in your mutation or reuse logic across multiple Convex functions you can define and call helper + +TypeScript + +functions: + +convex/myFunctions.ts + +TS + +``` +import { v } from "convex/values"; +import { mutation, MutationCtx } from "./_generated/server"; + +export const addItem = mutation({ + args: { text: v.string() }, + handler: async (ctx, args) => { + await ctx.db.insert("tasks", { text: args.text }); + await trackChange(ctx, "addItem"); + }, +}); + +async function trackChange(ctx: MutationCtx, type: "addItem" | "removeItem") { + await ctx.db.insert("changes", { type }); +} +``` + +Mutations can call helpers that take a [QueryCtx](/generated-api/server.md#queryctx) as argument, since the mutation context can do everything query context can. + +You can `export` helpers to use them across multiple files. They will not be callable from outside of your Convex functions. + +See [Type annotating server side helpers](/understanding/best-practices/typescript.md#type-annotating-server-side-helpers) for more guidance on TypeScript types. + +## Using NPM packages[​](#using-npm-packages "Direct link to Using NPM packages") + +Mutations can import NPM packages installed in `node_modules`. Not all NPM packages are supported, see [Runtimes](/functions/runtimes.md#default-convex-runtime) for more details. + +``` +npm install @faker-js/faker +``` + +convex/myFunctions.ts + +TS + +``` +import { faker } from "@faker-js/faker"; +import { mutation } from "./_generated/server"; + +export const randomName = mutation({ + args: {}, + handler: async (ctx) => { + faker.seed(); + await ctx.db.insert("tasks", { text: "Greet " + faker.person.fullName() }); + }, +}); +``` + +## Calling mutations from clients[​](#calling-mutations-from-clients "Direct link to Calling mutations from clients") + +To call a mutation from [React](/client/react.md) use the [`useMutation`](/client/react.md#editing-data) hook along with the generated [`api`](/generated-api/api.md) object. + +src/myApp.tsx + +TS + +``` +import { useMutation } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function MyApp() { + const mutateSomething = useMutation(api.myFunctions.mutateSomething); + const handleClick = () => { + mutateSomething({ a: 1, b: 2 }); + }; + // pass `handleClick` to a button + // ... +} +``` + +See the [React](/client/react.md) client documentation for all the ways queries can be called. + +When mutations are called from the [React](/client/react.md) or [Rust](/client/rust.md) clients, they are executed one at a time in a single, ordered queue. You don't have to worry about mutations editing the database in a different order than they were triggered. + +## Transactions[​](#transactions "Direct link to Transactions") + +Mutations run **transactionally**. This means that: + +1. All database reads inside the transaction get a consistent view of the data in the database. You don't have to worry about a concurrent update changing the data in the middle of the execution. +2. All database writes get committed together. If the mutation writes some data to the database, but later throws an error, no data is actually written to the database. + +For this to work, similarly to queries, mutations must be deterministic, and cannot call third party APIs. To call third party APIs, use [actions](/functions/actions.md). + +## Limits[​](#limits "Direct link to Limits") + +Mutations have a limit to the amount of data they can read and write at once to guarantee good performance. Learn more in [Read/write limit errors](/functions/error-handling/.md#readwrite-limit-errors). + +For information on other limits, see [Limits](/production/state/limits.md). diff --git a/.claude/docs/references/convex/functions/query.mdx b/.claude/docs/references/convex/functions/query.mdx new file mode 100644 index 00000000..3f3a5afd --- /dev/null +++ b/.claude/docs/references/convex/functions/query.mdx @@ -0,0 +1,530 @@ +# Queries + +Queries are the bread and butter of your backend API. They fetch data from the database, check authentication or perform other business logic, and return data back to the client application. + +This is an example query, taking in named arguments, reading data from the database and returning a result: + +convex/myFunctions.ts + +TS + +``` +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +// Return the last 100 tasks in a given task list. +export const getTaskList = query({ + args: { taskListId: v.id("taskLists") }, + handler: async (ctx, args) => { + const tasks = await ctx.db + .query("tasks") + .filter((q) => q.eq(q.field("taskListId"), args.taskListId)) + .order("desc") + .take(100); + return tasks; + }, +}); +``` + +Read on to understand how to build queries yourself. + +## Query names[​](#query-names "Direct link to Query names") + +Queries are defined in + +TypeScript + +files inside your `convex/` directory. + +The path and name of the file, as well as the way the function is exported from the file, determine the name the client will use to call it: + +convex/myFunctions.ts + +TS + +``` +// This function will be referred to as `api.myFunctions.myQuery`. +export const myQuery = …; + +// This function will be referred to as `api.myFunctions.sum`. +export const sum = …; +``` + +To structure your API you can nest directories inside the `convex/` directory: + +convex/foo/myQueries.ts + +TS + +``` +// This function will be referred to as `api.foo.myQueries.listMessages`. +export const listMessages = …; +``` + +Default exports receive the name `default`. + +convex/myFunctions.ts + +TS + +``` +// This function will be referred to as `api.myFunctions.default`. +export default …; +``` + +The same rules apply to [mutations](/functions/mutation-functions.md) and [actions](/functions/actions.md), while [HTTP actions](/functions/http-actions.md) use a different routing approach. + +Client libraries in languages other than JavaScript and TypeScript use strings instead of API objects: + +* `api.myFunctions.myQuery` is `"myFunctions:myQuery"` +* `api.foo.myQueries.myQuery` is `"foo/myQueries:myQuery"`. +* `api.myFunction.default` is `"myFunction:default"` or `"myFunction"`. + +## The `query` constructor[​](#the-query-constructor "Direct link to the-query-constructor") + +To actually declare a query in Convex you use the `query` constructor function. Pass it an object with a `handler` function, which returns the query result: + +convex/myFunctions.ts + +TS + +``` +import { query } from "./_generated/server"; + +export const myConstantString = query({ + args: {}, + handler: () => { + return "My never changing string"; + }, +}); +``` + +### Query arguments[​](#query-arguments "Direct link to Query arguments") + +Queries accept named arguments. The argument values are accessible as fields of the second parameter of the handler function: + +convex/myFunctions.ts + +TS + +``` +import { query } from "./_generated/server"; + +export const sum = query({ + handler: (_, args: { a: number; b: number }) => { + return args.a + args.b; + }, +}); +``` + +Arguments and responses are automatically serialized and deserialized, and you can pass and return most value-like JavaScript data to and from your query. + +To both declare the types of arguments and to validate them, add an `args` object using `v` validators: + +convex/myFunctions.ts + +TS + +``` +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const sum = query({ + args: { a: v.number(), b: v.number() }, + handler: (_, args) => { + return args.a + args.b; + }, +}); +``` + +See [argument validation](/functions/validation.md) for the full list of supported types and validators. + +The first parameter of the handler function contains the query context. + +### Query responses[​](#query-responses "Direct link to Query responses") + +Queries can return values of any supported [Convex type](/functions/validation.md) which will be automatically serialized and deserialized. + +Queries can also return `undefined`, which is not a valid Convex value. When a query returns `undefined` **it is translated to `null`** on the client. + +### Query context[​](#query-context "Direct link to Query context") + +The `query` constructor enables fetching data, and other Convex features by passing a [QueryCtx](/generated-api/server.md#queryctx) object to the handler function as the first parameter: + +convex/myFunctions.ts + +TS + +``` +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const myQuery = query({ + args: { a: v.number(), b: v.number() }, + handler: (ctx, args) => { + // Do something with `ctx` + }, +}); +``` + +Which part of the query context is used depends on what your query needs to do: + +* To fetch from the database use the `db` field. Note that we make the handler function an `async` function so we can `await` the promise returned by `db.get()`: + + convex/myFunctions.ts + + TS + + ``` + import { query } from "./_generated/server"; + import { v } from "convex/values"; + + export const getTask = query({ + args: { id: v.id("tasks") }, + handler: async (ctx, args) => { + return await ctx.db.get("tasks", args.id); + }, + }); + ``` + + Read more about [Reading Data](/database/reading-data/.md). + +* To return URLs to stored files use the `storage` field. Read more about [File Storage](/file-storage.md). + +* To check user authentication use the `auth` field. Read more about [Authentication](/auth.md). + +## Splitting up query code via helpers[​](#splitting-up-query-code-via-helpers "Direct link to Splitting up query code via helpers") + +When you want to split up the code in your query or reuse logic across multiple Convex functions you can define and call helper + +TypeScript + +functions: + +convex/myFunctions.ts + +TS + +``` +import { Id } from "./_generated/dataModel"; +import { query, QueryCtx } from "./_generated/server"; +import { v } from "convex/values"; + +export const getTaskAndAuthor = query({ + args: { id: v.id("tasks") }, + handler: async (ctx, args) => { + const task = await ctx.db.get("tasks", args.id); + if (task === null) { + return null; + } + return { task, author: await getUserName(ctx, task.authorId ?? null) }; + }, +}); + +async function getUserName(ctx: QueryCtx, userId: Id<"users"> | null) { + if (userId === null) { + return null; + } + return (await ctx.db.get("users", userId))?.name; +} +``` + +You can `export` helpers to use them across multiple files. They will not be callable from outside of your Convex functions. + +See [Type annotating server side helpers](/understanding/best-practices/typescript.md#type-annotating-server-side-helpers) for more guidance on TypeScript types. + +## Using NPM packages[​](#using-npm-packages "Direct link to Using NPM packages") + +Queries can import NPM packages installed in `node_modules`. Not all NPM packages are supported, see [Runtimes](/functions/runtimes.md#default-convex-runtime) for more details. + +``` +npm install @faker-js/faker +``` + +convex/myFunctions.ts + +TS + +``` +import { query } from "./_generated/server"; +import { faker } from "@faker-js/faker"; + +export const randomName = query({ + args: {}, + handler: () => { + faker.seed(); + return faker.person.fullName(); + }, +}); +``` + +## Calling queries from clients[​](#calling-queries-from-clients "Direct link to Calling queries from clients") + +To call a query from [React](/client/react.md) use the [`useQuery`](/client/react.md#fetching-data) hook along with the generated [`api`](/generated-api/api.md) object. + +src/MyApp.tsx + +TS + +``` +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function MyApp() { + const data = useQuery(api.myFunctions.sum, { a: 1, b: 2 }); + // do something with `data` +} +``` + +See the [React](/client/react.md) client documentation for all the ways queries can be called. + +## Caching & reactivity & consistency[​](#caching--reactivity--consistency "Direct link to Caching & reactivity & consistency") + +Queries have three awesome attributes: + +1. **Caching**: Convex caches query results automatically. If many clients request the same query, with the same arguments, they will receive a cached response. +2. **Reactivity**: clients can subscribe to queries to receive new results when the underlying data changes. +3. **Consistency**: All database reads inside a single query call are performed at the same logical timestamp. Concurrent writes do not affect the query results. + +To have these attributes the handler function must be *deterministic*, which means that given the same arguments (including the query context) it will return the same response. + +For this reason queries cannot `fetch` from third party APIs. To call third party APIs, use [actions](/functions/actions.md). + +You might wonder whether you can use non-deterministic language functionality like `Math.random()` or `Date.now()`. The short answer is that Convex takes care of implementing these in a way that you don't have to think about the deterministic constraint. + +See [Runtimes](/functions/runtimes.md#default-convex-runtime) for more details on the Convex runtime. + +## Limits[​](#limits "Direct link to Limits") + +Queries have a limit to the amount of data they can read at once to guarantee good performance. Check out these limits in [Read/write limit errors](/functions/error-handling/.md#readwrite-limit-errors). + +For information on other limits, see [Limits](/production/state/limits.md). + +# Paginated Queries + +Paginated queries are [queries](/functions/query-functions.md) that return a list of results in incremental pages. + +This can be used to build components with "Load More" buttons or "infinite scroll" UIs where more results are loaded as the user scrolls. + +**Example:** [Paginated Messaging App](https://github.com/get-convex/convex-demos/tree/main/pagination) + +Using pagination in Convex is as simple as: + +1. Writing a paginated query function that calls [`.paginate(paginationOpts)`](/api/interfaces/server.OrderedQuery.md#paginate). +2. Using the [`usePaginatedQuery`](/api/modules/react.md#usepaginatedquery) React hook. + +Like other Convex queries, paginated queries are completely reactive. + +## Writing paginated query functions[​](#writing-paginated-query-functions "Direct link to Writing paginated query functions") + +Convex uses cursor-based pagination. This means that paginated queries return a string called a [`Cursor`](/api/modules/server.md#cursor) that represents the point in the results that the current page ended. To load more results, you simply call the query function again, passing in the cursor. + +To build this in Convex, define a query function that: + +1. Takes in a single arguments object with a `paginationOpts` property of type [`PaginationOptions`](/api/interfaces/server.PaginationOptions.md). + + + + * `PaginationOptions` is an object with `numItems` and `cursor` fields. + * Use `paginationOptsValidator` exported from `"convex/server"` to [validate](/functions/validation.md) this argument + * The arguments object may include properties as well. + +2. Calls [`.paginate(paginationOpts)`](/api/interfaces/server.OrderedQuery.md#paginate) on a [database query](/database/reading-data/.md), passing in the `PaginationOptions` and returning its result. + + * The returned `page` in the [`PaginationResult`](/api/interfaces/server.PaginationResult.md) is an array of documents. You may [`map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) or [`filter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) it before returning it. + +convex/messages.ts + +TS + +``` +import { v } from "convex/values"; +import { query, mutation } from "./_generated/server"; +import { paginationOptsValidator } from "convex/server"; + +export const list = query({ + args: { paginationOpts: paginationOptsValidator }, + handler: async (ctx, args) => { + const foo = await ctx.db + .query("messages") + .order("desc") + .paginate(args.paginationOpts); + return foo; + }, +}); +``` + +### Additional arguments[​](#additional-arguments "Direct link to Additional arguments") + +You can define paginated query functions that take arguments in addition to `paginationOpts`: + +convex/messages.ts + +TS + +``` +export const listWithExtraArg = query({ + args: { paginationOpts: paginationOptsValidator, author: v.string() }, + handler: async (ctx, args) => { + return await ctx.db + .query("messages") + .filter((q) => q.eq(q.field("author"), args.author)) + .order("desc") + .paginate(args.paginationOpts); + }, +}); +``` + +### Transforming results[​](#transforming-results "Direct link to Transforming results") + +You can apply arbitrary [transformations](/database/reading-data/.md#more-complex-queries) to the `page` property of the object returned by `paginate`, which contains the array of documents: + +convex/messages.ts + +TS + +``` +export const listWithTransformation = query({ + args: { paginationOpts: paginationOptsValidator }, + handler: async (ctx, args) => { + const results = await ctx.db + .query("messages") + .order("desc") + .paginate(args.paginationOpts); + return { + ...results, + page: results.page.map((message) => ({ + author: message.author.slice(0, 1), + body: message.body.toUpperCase(), + })), + }; + }, +}); +``` + +## Paginating within React Components[​](#paginating-within-react-components "Direct link to Paginating within React Components") + +To paginate within a React component, use the [`usePaginatedQuery`](/api/modules/react.md#usepaginatedquery) hook. This hook gives you a simple interface for rendering the current items and requesting more. Internally, this hook manages the continuation cursors. + +The arguments to this hook are: + +* The name of the paginated query function. +* The arguments object to pass to the query function, excluding the `paginationOpts` (that's injected by the hook). +* An options object with the `initialNumItems` to load on the first page. + +The hook returns an object with: + +* `results`: An array of the currently loaded results. + +* `isLoading` - Whether the hook is currently loading results. + +* `status`: The status of the pagination. The possible statuses are: + + + + * `"LoadingFirstPage"`: The hook is loading the first page of results. + * `"CanLoadMore"`: This query may have more items to fetch. Call `loadMore` to fetch another page. + * `"LoadingMore"`: We're currently loading another page of results. + * `"Exhausted"`: We've paginated to the end of the list. + +* `loadMore(n)`: A callback to fetch more results. This will only fetch more results if the status is `"CanLoadMore"`. + +src/App.tsx + +TS + +``` +import { usePaginatedQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const { results, status, loadMore } = usePaginatedQuery( + api.messages.list, + {}, + { initialNumItems: 5 }, + ); + return ( +
+ {results?.map(({ _id, body }) =>
{body}
)} + +
+ ); +} +``` + +You can also pass additional arguments in the arguments object if your function expects them: + +src/App.tsx + +TS + +``` +import { usePaginatedQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function App() { + const { results, status, loadMore } = usePaginatedQuery( + api.messages.listWithExtraArg, + { author: "Alex" }, + { initialNumItems: 5 }, + ); + return ( +
+ {results?.map(({ _id, body }) =>
{body}
)} + +
+ ); +} +``` + +### Reactivity[​](#reactivity "Direct link to Reactivity") + +Like any other Convex query functions, paginated queries are **completely reactive**. Your React components will automatically rerender if items in your paginated list are added, removed or changed. + +One consequence of this is that **page sizes in Convex may change!** If you request a page of 10 items and then one item is removed, this page may "shrink" to only have 9 items. Similarly if new items are added, a page may "grow" beyond its initial size. + +## Paginating manually[​](#paginating-manually "Direct link to Paginating manually") + +If you're paginating outside of React, you can manually call your paginated function multiple times to collect the items: + +download.ts + +TS + +``` +import { ConvexHttpClient } from "convex/browser"; +import { api } from "../convex/_generated/api"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +const client = new ConvexHttpClient(process.env.VITE_CONVEX_URL!); + +/** + * Logs an array containing all messages from the paginated query "listMessages" + * by combining pages of results into a single array. + */ +async function getAllMessages() { + let continueCursor = null; + let isDone = false; + let page; + + const results = []; + + while (!isDone) { + ({ continueCursor, isDone, page } = await client.query(api.messages.list, { + paginationOpts: { numItems: 5, cursor: continueCursor }, + })); + console.log("got", page.length); + results.push(...page); + } + + console.log(results); +} + +getAllMessages(); +``` diff --git a/.claude/docs/references/convex/functions/validation.mdx b/.claude/docs/references/convex/functions/validation.mdx new file mode 100644 index 00000000..8c31aa60 --- /dev/null +++ b/.claude/docs/references/convex/functions/validation.mdx @@ -0,0 +1,249 @@ +# Argument and Return Value Validation + +Argument and return value validators ensure that [queries](/functions/query-functions.md), [mutations](/functions/mutation-functions.md), and [actions](/functions/actions.md) are called with the correct types of arguments and return the expected types of return values. + +**This is important for security!** Without argument validation, a malicious user can call your public functions with unexpected arguments and cause surprising results. [TypeScript](/understanding/best-practices/typescript.md) alone won't help because TypeScript types aren't present at runtime. We recommend adding argument validation for all public functions in production apps. For non-public functions that are not called by clients, we recommend [internal functions](/functions/internal-functions.md) and optionally validation. + +**Example:** [Argument Validation](https://github.com/get-convex/convex-demos/tree/main/args-validation) + +## Adding validators[​](#adding-validators "Direct link to Adding validators") + +To add argument validation to your functions, pass an object with `args` and `handler` properties to the `query`, `mutation` or `action` constructor. To add return value validation, use the `returns` property in this object: + +convex/message.ts + +TS + +``` +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +export const send = mutation({ + args: { + body: v.string(), + author: v.string(), + }, + returns: v.null(), + handler: async (ctx, args) => { + const { body, author } = args; + await ctx.db.insert("messages", { body, author }); + }, +}); +``` + +If you define your function with an argument validator, there is no need to include [TypeScript](/understanding/best-practices/typescript.md) type annotations! The type of your function will be inferred automatically. Similarly, if you define a return value validator, the return type of your function will be inferred from the validator, and TypeScript will check that it matches the inferred return type of the `handler` function. + +Unlike TypeScript, validation for an object will throw if the object contains properties that are not declared in the validator. + +If the client supplies arguments not declared in `args`, or if the function returns a value that does not match the validator declared in `returns`. This is helpful to prevent bugs caused by mistyped names of arguments or returning more data than intended to a client. + +Even `args: {}` is a helpful use of validators because TypeScript will show an error on the client if you try to pass any arguments to the function which doesn't expect them. + +## Supported types[​](#supported-types "Direct link to Supported types") + +All functions, both public and internal, can accept and return the following data types. Each type has a corresponding validator that can be accessed on the [`v`](/api/modules/values.md#v) object imported from `"convex/values"`. + +The [database](/database.md) can store the exact same set of [data types](/database/types.md). + +Additionally you can also express type unions, literals, `any` types, and optional fields. + +### Convex values[​](#convex-values "Direct link to Convex values") + +Convex supports the following types of values: + +| Convex Type | TS/JS Type | Example Usage | Validator for [Argument Validation](/functions/validation.md) and [Schemas](/database/schemas.md) | `json` Format for [Export](/database/import-export/.md) | Notes | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Id | [Id](/database/document-ids.md) ([string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type)) | `doc._id` | `v.id(tableName)` | string | See [Document IDs](/database/document-ids.md). | +| Null | [null](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#null_type) | `null` | `v.null()` | null | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. | +| Int64 | [bigint](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#bigint_type) | `3n` | `v.int64()` | string (base10) | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in [most modern browsers](https://caniuse.com/bigint). | +| Float64 | [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#number_type) | `3.1` | `v.number()` | number / string | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. | +| Boolean | [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#boolean_type) | `true` | `v.boolean()` | bool | | +| String | [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#string_type) | `"abc"` | `v.string()` | string | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. | +| Bytes | [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) | `new ArrayBuffer(8)` | `v.bytes()` | string (base64) | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. | +| Array | [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) | `[1, 3.2, "abc"]` | `v.array(values)` | array | Arrays can have at most 8192 values. | +| Object | [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#objects) | `{a: "abc"}` | `v.object({property: value})` | object | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Convex includes all [enumerable properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "\_". | +| Record | [Record](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type) | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | object | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". | + +### Unions[​](#unions "Direct link to Unions") + +You can describe fields that could be one of multiple types using `v.union`: + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ + args: { + stringOrNull: v.union(v.string(), v.null()), + }, + handler: async (ctx, { stringOrNull }) => { + //... + }, +}); +``` + +For convenience, `v.nullable(foo)` is equivalent to `v.union(foo, v.null())`. + +### Literals[​](#literals "Direct link to Literals") + +Fields that are a constant can be expressed with `v.literal`. This is especially useful when combined with unions: + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ + args: { + oneTwoOrThree: v.union( + v.literal("one"), + v.literal("two"), + v.literal("three"), + ), + }, + handler: async (ctx, { oneTwoOrThree }) => { + //... + }, +}); +``` + +### Record objects[​](#record-objects "Direct link to Record objects") + +You can describe objects that map arbitrary keys to values with `v.record`: + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ + args: { + simpleMapping: v.record(v.string(), v.boolean()), + }, + handler: async (ctx, { simpleMapping }) => { + //... + }, +}); +``` + +You can use other types of string validators for the keys: + +``` +defineTable({ + userIdToValue: v.record(v.id("users"), v.boolean()), +}); +``` + +Notes: + +* This type corresponds to the [Record\](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type) type in TypeScript. +* You cannot use string literals as a `record` key. +* Using `v.string()` as a `record` key validator will only allow ASCII characters. + +### Any[​](#any "Direct link to Any") + +Fields that could take on any value can be represented with `v.any()`: + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ + args: { + anyValue: v.any(), + }, + handler: async (ctx, { anyValue }) => { + //... + }, +}); +``` + +This corresponds to the `any` type in TypeScript. + +### Optional fields[​](#optional-fields "Direct link to Optional fields") + +You can describe optional fields by wrapping their type with `v.optional(...)`: + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ + args: { + optionalString: v.optional(v.string()), + optionalNumber: v.optional(v.number()), + }, + handler: async (ctx, { optionalString, optionalNumber }) => { + //... + }, +}); +``` + +This corresponds to marking fields as optional with `?` in TypeScript. + +## Extracting TypeScript types[​](#extracting-typescript-types "Direct link to Extracting TypeScript types") + +The [`Infer`](/api/modules/values.md#infer) type allows you to turn validator calls into TypeScript types. This can be useful to remove duplication between your validators and TypeScript types: + +``` +import { mutation } from "./_generated/server"; +import { Infer, v } from "convex/values"; + +const nestedObject = v.object({ + property: v.string(), +}); + +// Resolves to `{property: string}`. +export type NestedObject = Infer; + +export default mutation({ + args: { + nested: nestedObject, + }, + handler: async (ctx, { nested }) => { + //... + }, +}); +``` + +### Reusing and extending validators[​](#reusing-and-extending-validators "Direct link to Reusing and extending validators") + +Validators can be defined once and shared between functions and table schemas. + +``` +const statusValidator = v.union(v.literal("active"), v.literal("inactive")); + +const userValidator = v.object({ + name: v.string(), + email: v.email(), + status: statusValidator, + profileUrl: v.optional(v.string()), +}); + +const schema = defineSchema({ + users: defineTable(userValidator).index("by_email", ["email"]), +}); +``` + +You can create new object validators from existing ones by adding or removing fields using `.pick`, `.omit`, `.extend`, and `.partial` on object validators. + +``` +// Creates a new validator with only the name and profileUrl fields. +const publicUser = userValidator.pick("name", "profileUrl"); + +// Creates a new validator with all fields except the specified fields. +const userWithoutStatus = userValidator.omit("status", "profileUrl"); + +// Creates a validator where all fields are optional. +// This is useful for validating patches to a document. +const userPatch = userWithoutStatus.partial(); + +// Creates a new validator adding system fields to the user validator. +const userDocument = userValidator.extend({ + _id: v.id("users"), + _creationTime: v.number(), +}); +``` + +Notes: + +* Object validators don't allow extra properties, objects with properties that aren't specified will fail validation. +* Top-level table fields cannot start with `_` because they are reserved for system fields like `_id` and `_creationTime`. diff --git a/.claude/docs/references/convex/quickstart.mdx b/.claude/docs/references/convex/quickstart.mdx new file mode 100644 index 00000000..83d4a334 --- /dev/null +++ b/.claude/docs/references/convex/quickstart.mdx @@ -0,0 +1,198 @@ +# Next.js Quickstart + +Convex + Next.js + +Convex is an all-in-one backend and database that integrates quickly and easily with Next.js. + +Once you've gotten started, see how to set up [hosting](/production/hosting/.md), [server rendering](/client/nextjs/app-router/server-rendering.md), and [auth](https://docs.convex.dev/client/nextjs/). + +To get setup quickly with Convex and Next.js run + +**`npm create convex@latest`** + +**``** + +or follow the guide below. + +*** + +Learn how to query data from Convex in a Next.js app using the App Router and + +TypeScript + +Alternatively see the [Pages Router](/client/nextjs/pages-router/quickstart.md) version of this quickstart. + +1. Create a Next.js app + + Create a Next.js app using the `npx create-next-app` command. + + Choose the default option for every prompt (hit Enter). + + ``` + npx create-next-app@latest my-app + ``` + +2. Install the Convex client and server library + + To get started, install the `convex` package. + + Navigate to your app and install `convex`. + + ``` + cd my-app && npm install convex + ``` + +3. Set up a Convex dev deployment + + Next, run `npx convex dev`. This will prompt you to log in with GitHub, create a project, and save your production and deployment URLs. + + It will also create a `convex/` folder for you to write your backend API functions in. The `dev` command will then continue running to sync your functions with your dev deployment in the cloud. + + ``` + npx convex dev + ``` + +4. Create sample data for your database + + In a new terminal window, create a `sampleData.jsonl` file with some sample data. + + sampleData.jsonl + + ``` + {"text": "Buy groceries", "isCompleted": true} + {"text": "Go for a swim", "isCompleted": true} + {"text": "Integrate Convex", "isCompleted": false} + ``` + +5. Add the sample data to your database + + Use the [`import`](/database/import-export/import.md) command to add a `tasks` table with the sample data into your Convex database. + + ``` + npx convex import --table tasks sampleData.jsonl + ``` + +6. Expose a database query + + In the `convex/` folder, add a new file `tasks.ts` with a query function that loads the data. + + Exporting a query function from this file declares an API function named after the file and the export name: `api.tasks.get`. + + convex/tasks.ts + + TS + + ``` + import { query } from "./_generated/server"; + + export const get = query({ + args: {}, + handler: async (ctx) => { + return await ctx.db.query("tasks").collect(); + }, + }); + ``` + +7. Create a client component for the Convex provider + + For `` to work on the client, `ConvexReactClient` must be passed to it. + + In the `app/` folder, add a new file `ConvexClientProvider.tsx` with the following code. This creates a client component that wraps `` and passes it the ``. + + app/ConvexClientProvider.tsx + + TS + + ``` + "use client"; + + import { ConvexProvider, ConvexReactClient } from "convex/react"; + import { ReactNode } from "react"; + + const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + + export function ConvexClientProvider({ children }: { children: ReactNode }) { + return {children}; + } + ``` + +8. Wire up the ConvexClientProvider + + In `app/layout.tsx`, wrap the children of the `body` element with the ``. + + app/layout.tsx + + TS + + ``` + import type { Metadata } from "next"; + import { Geist, Geist_Mono } from "next/font/google"; + import "./globals.css"; + import { ConvexClientProvider } from "./ConvexClientProvider"; + + const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], + }); + + const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], + }); + + export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", + }; + + export default function RootLayout({ + children, + }: Readonly<{ + children: React.ReactNode; + }>) { + return ( + + + {children} + + + ); + } + ``` + +9. Display the data in your app + + In `app/page.tsx`, use the `useQuery()` hook to fetch from your `api.tasks.get` API function. + + app/page.tsx + + TS + + ``` + "use client"; + + import Image from "next/image"; + import { useQuery } from "convex/react"; + import { api } from "../convex/_generated/api"; + + export default function Home() { + const tasks = useQuery(api.tasks.get); + return ( +
+ {tasks?.map(({ _id, text }) =>
{text}
)} +
+ ); + } + ``` + +10. Start the app + + Run your Next.js development server, open in a browser, and see the list of tasks. + + ``` + npm run dev + ``` + +See the complete [Next.js documentation](/client/nextjs/app-router/.md). diff --git a/.claude/docs/references/convex/typescript.mdx b/.claude/docs/references/convex/typescript.mdx new file mode 100644 index 00000000..1554eb85 --- /dev/null +++ b/.claude/docs/references/convex/typescript.mdx @@ -0,0 +1,223 @@ +# TypeScript + +Convex provides end-to-end type support when Convex functions are written in [TypeScript](https://www.typescriptlang.org/). + +You can gradually add TypeScript to a Convex project: the following steps provide progressively better type support. For the best support you'll want to complete them all. + +**Example:** [TypeScript and Schema](https://github.com/get-convex/convex-demos/tree/main/typescript) + +## Writing Convex functions in TypeScript[​](#writing-convex-functions-in-typescript "Direct link to Writing Convex functions in TypeScript") + +The first step to improving type support in a Convex project is to writing your Convex functions in TypeScript by using the `.ts` extension. + +If you are using [argument validation](/functions/validation.md), Convex will infer the types of your functions arguments automatically: + +convex/sendMessage.ts + +``` +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export default mutation({ + args: { + body: v.string(), + author: v.string(), + }, + // Convex knows that the argument type is `{body: string, author: string}`. + handler: async (ctx, args) => { + const { body, author } = args; + await ctx.db.insert("messages", { body, author }); + }, +}); +``` + +Otherwise you can annotate the arguments type manually: + +convex/sendMessage.ts + +``` +import { internalMutation } from "./_generated/server"; + +export default internalMutation({ + // To convert this function from JavaScript to + // TypeScript you annotate the type of the arguments object. + handler: async (ctx, args: { body: string; author: string }) => { + const { body, author } = args; + await ctx.db.insert("messages", { body, author }); + }, +}); +``` + +This can be useful for [internal functions](/functions/internal-functions.md) accepting complicated types. + +If TypeScript is installed in your project `npx convex dev` and `npx convex deploy` will typecheck Convex functions before sending code to the Convex backend. + +Convex functions are typechecked with the `tsconfig.json` in the Convex folder: you can modify some parts of this file to change typechecking settings, or delete this file to disable this typecheck. + +You'll find most database methods have a return type of `Promise` until you add a schema. + +## Adding a schema[​](#adding-a-schema "Direct link to Adding a schema") + +Once you [define a schema](/database/schemas.md) the type signature of database methods will be known. You'll also be able to use types imported from `convex/_generated/dataModel` in both Convex functions and clients written in TypeScript (React, React Native, Node.js etc.). + +The types of documents in tables can be described using the [`Doc`](/generated-api/data-model.md#doc) type from the generated data model and references to documents can be described with parametrized [Document IDs](/database/document-ids.md). + +convex/messages.ts + +``` +import { query } from "./_generated/server"; + +export const list = query({ + args: {}, + // The inferred return type of `handler` is now `Promise[]>` + handler: (ctx) => { + return ctx.db.query("messages").collect(); + }, +}); +``` + +## Type annotating server-side helpers[​](#type-annotating-server-side-helpers "Direct link to Type annotating server-side helpers") + +When you want to reuse logic across Convex functions you'll want to define helper TypeScript functions, and these might need some of the provided context, to access the database, authentication and any other Convex feature. + +Convex generates types corresponding to documents and IDs in your database, `Doc` and `Id`, as well as `QueryCtx`, `MutationCtx` and `ActionCtx` types based on your schema and declared Convex functions: + +convex/helpers.ts + +``` +// Types based on your schema +import { Doc, Id } from "./_generated/dataModel"; +// Types based on your schema and declared functions +import { + QueryCtx, + MutationCtx, + ActionCtx, + DatabaseReader, + DatabaseWriter, +} from "./_generated/server"; +// Types that don't depend on schema or function +import { + Auth, + StorageReader, + StorageWriter, + StorageActionWriter, +} from "convex/server"; + +// Note that a `MutationCtx` also satisfies the `QueryCtx` interface +export function myReadHelper(ctx: QueryCtx, id: Id<"channels">) { + /* ... */ +} + +export function myActionHelper(ctx: ActionCtx, doc: Doc<"messages">) { + /* ... */ +} +``` + +### Inferring types from validators[​](#inferring-types-from-validators "Direct link to Inferring types from validators") + +Validators can be reused between [argument validation](/functions/validation.md) and [schema validation](/database/schemas.md). You can use the provided [`Infer`](/api/modules/values.md#infer) type to get a TypeScript type corresponding to a validator: + +convex/helpers.ts + +``` +import { Infer, v } from "convex/values"; + +export const courseValidator = v.union( + v.literal("appetizer"), + v.literal("main"), + v.literal("dessert"), +); + +// The corresponding type can be used in server or client-side helpers: +export type Course = Infer; +// is inferred as `'appetizer' | 'main' | 'dessert'` +``` + +### Document types without system fields[​](#document-types-without-system-fields "Direct link to Document types without system fields") + +All documents in Convex include the built-in `_id` and `_creationTime` fields, and so does the generated `Doc` type. When creating or updating a document you might want use the type without the system fields. Convex provides [`WithoutSystemFields`](/api/modules/server.md#withoutsystemfields) for this purpose: + +convex/helpers.ts + +``` +import { MutationCtx } from "./_generated/server"; +import { WithoutSystemFields } from "convex/server"; +import { Doc } from "./_generated/dataModel"; + +export async function insertMessageHelper( + ctx: MutationCtx, + values: WithoutSystemFields>, +) { + // ... + await ctx.db.insert("messages", values); + // ... +} +``` + +## Writing frontend code in TypeScript[​](#writing-frontend-code-in-typescript "Direct link to Writing frontend code in TypeScript") + +All Convex JavaScript clients, including React hooks like [`useQuery`](/api/modules/react.md#usequery) and [`useMutation`](/api/modules/react.md#usemutation) provide end to end type safety by ensuring that arguments and return values match the corresponding Convex functions declarations. For React, install and configure TypeScript so you can write your React components in `.tsx` files instead of `.jsx` files. + +Follow our [React](/quickstart/react.md) or [Next.js](/quickstart/nextjs.md) quickstart to get started with Convex and TypeScript. + +### Type annotating client-side code[​](#type-annotating-client-side-code "Direct link to Type annotating client-side code") + +When you want to pass the result of calling a function around your client codebase, you can use the generated types `Doc` and `Id`, just like on the backend: + +src/App.tsx + +``` +import { Doc, Id } from "../convex/_generated/dataModel"; + +function Channel(props: { channelId: Id<"channels"> }) { + // ... +} + +function MessagesView(props: { message: Doc<"messages"> }) { + // ... +} +``` + +You can also declare custom types inside your backend codebase which include `Doc`s and `Id`s, and import them in your client-side code. + +You can also use `WithoutSystemFields` and any types inferred from validators via `Infer`. + +#### Using inferred function return types[​](#using-inferred-function-return-types "Direct link to Using inferred function return types") + +Sometimes you might want to annotate a type on the client based on whatever your backend function returns. Beside manually declaring the type (on the backend or on the frontend), you can use the generic `FunctionReturnType` and `UsePaginatedQueryReturnType` types with a function reference: + +src/Components.tsx + +``` +import { FunctionReturnType } from "convex/server"; +import { UsePaginatedQueryReturnType } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function MyHelperComponent(props: { + data: FunctionReturnType; +}) { + // ... +} + +export function MyPaginationHelperComponent(props: { + paginatedData: UsePaginatedQueryReturnType< + typeof api.myFunctions.getSomethingPaginated + >; +}) { + // ... +} +``` + +## Turning `string`s into valid document IDs[​](#turning-strings-into-valid-document-ids "Direct link to turning-strings-into-valid-document-ids") + +See [Serializing IDs](/database/document-ids.md#serializing-ids). + +## Required TypeScript version[​](#required-typescript-version "Direct link to Required TypeScript version") + +Convex requires TypeScript version [5.0.3](https://www.npmjs.com/package/typescript/v/5.0.3) or newer. + +Related posts from + + + +[![Stack](/img/stack-logo-dark.svg)![Stack](/img/stack-logo-light.svg)](https://stack.convex.dev/) diff --git a/.claude/docs/references/convex/understanding.mdx b/.claude/docs/references/convex/understanding.mdx new file mode 100644 index 00000000..0f1cb63b --- /dev/null +++ b/.claude/docs/references/convex/understanding.mdx @@ -0,0 +1,172 @@ +# Convex Overview + +Convex is the open source, reactive database where queries are TypeScript code running right in the database. Just like React components react to state changes, Convex queries react to database changes. + +Convex provides a database, a place to write your server functions, and client libraries. It makes it easy to build and scale dynamic live-updating apps. + +The following diagram shows the standard three-tier app architecture that Convex enables. We'll start at the bottom and work our way up to the top of this diagram. + +![Convex in your app](/assets/images/basic-diagram-8ad312f058c3cf7e15c3396e46eedb48.png) + +## Database[​](#database "Direct link to Database") + +The [database](/database.md) is at the core of Convex. The Convex database is automatically provisioned when you create your project. There is no connection setup or cluster management. + +info + +In Convex, your database queries are just [TypeScript code](/database/reading-data/.md) written in your [server functions](/functions.md). There is no SQL to write. There are no ORMs needed. + +The Convex database is reactive. Whenever any data on which a query depends changes, the query is rerun, and client subscriptions are updated. + +Convex is a "document-relational" database. "Document" means you put JSON-like nested objects into your database. "Relational" means you have tables with relations, like `tasks` assigned to a `user` using IDs to reference documents in other tables. + +The Convex cloud offering runs on top of Amazon RDS using MySQL as its persistence layer. The Open Source version uses SQLite, Postgres and MySQL. The database is ACID-compliant and uses [serializable isolation and optimistic concurrency control](/database/advanced/occ.md). All that to say, Convex provides the strictest possible transactional guarantees, and you never see inconsistent data. + +## Server functions[​](#server-functions "Direct link to Server functions") + +When you create a new Convex project, you automatically get a `convex/` folder where you write your [server functions](/functions.md). This is where all your backend application logic and database query code live. + +Example TypeScript server functions that read (query) and write (mutation) to the database. + +convex/tasks.ts + +``` +// A Convex query function +export const getAllOpenTasks = query({ + args: {}, + handler: async (ctx, args) => { + // Query the database to get all items that are not completed + const tasks = await ctx.db + .query("tasks") + .withIndex("by_completed", (q) => q.eq("completed", false)) + .collect(); + return tasks; + }, +}); + +// A Convex mutation function +export const setTaskCompleted = mutation({ + args: { taskId: v.id("tasks"), completed: v.boolean() }, + handler: async (ctx, { taskId, completed }) => { + // Update the database using TypeScript + await ctx.db.patch("tasks", taskId, { completed }); + }, +}); +``` + +You read and write to your database through query or mutation functions. [Query functions](/functions/query-functions.md) are pure functions that can only read from the database. [Mutation functions](/functions/mutation-functions.md) are transactions that can read or write from the database. These two database functions are [not allowed to take any non-deterministic](/functions/runtimes.md#restrictions-on-queries-and-mutations) actions like network requests to ensure transactional guarantees. + +info + +The entire Convex mutation function is a transaction. There are no `begin` or `end` transaction statements to write. Convex automatically retries the function on conflicts, and you don't have to manage anything. + +Convex also provides standard general-purpose serverless functions called actions. [Action functions](/functions/actions.md) can make network requests. They have to call query or mutation functions to read and write to the database. You use actions to call LLMs or send emails. + +You can also durably schedule Convex functions via the [scheduler](/scheduling/scheduled-functions.md) or [cron jobs](/scheduling/cron-jobs.md). Scheduling lets you build workflows like emailing a new user a day later if they haven't performed an onboarding task. + +You call your Convex functions via [client libraries](/client/react.md) or directly via [HTTP](/http-api/.md#functions-api). + +## Client libraries[​](#client-libraries "Direct link to Client libraries") + +Convex client libraries keep your frontend synced with the results of your server functions. + +``` +// In your React component +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function TaskList() { + const data = useQuery(api.tasks.getAllOpenTasks); + return data ?? "Loading..."; +} +``` + +Like the `useState` hook that updates your React component when local state changes, the Convex `useQuery` hook automatically updates your component whenever the result of your query changes. There's no manual subscription management or state synchronization needed. + +When calling query functions, the client library subscribes to the results of the function. Convex tracks the dependencies of your query functions, including what data was read from the database. Whenever relevant data in the database changes, the Convex automatically reruns the query and sends the result to the client. + +The client library also queues up mutations in memory to send to the server. As mutations execute and cause query results to update, the client library keeps your app state consistent. It updates all subscriptions to the same logical moment in time in the database. + +Convex provides client libraries for nearly all popular web and native app frameworks. Client libraries connect to your Convex deployment via WebSockets. You can then call your public Convex functions [through the library](/client/react.md#fetching-data). You can also use Convex with [HTTP directly](/http-api/.md#functions-api), you just won't get the automatic subscriptions. + +## Putting it all together[​](#putting-it-all-together "Direct link to Putting it all together") + +Let's return to the `getAllOpenTasks` Convex query function from earlier that gets all tasks that are not marked as `completed`: + +convex/tasks.ts + +``` +export const getAllOpenTasks = query({ + args: {}, + handler: async (ctx, args) => { + // Query the database to get all items that are not completed + const tasks = await ctx.db + .query("tasks") + .withIndex("by_completed", (q) => q.eq("completed", false)) + .collect(); + return tasks; + }, +}); +``` + +Let's follow along what happens when you subscribe to this query: + +![Convex data flow](/assets/images/convex-query-subscription-945e7990515e438ab4385f9b4803bbd4.png) + +The web app uses the `useQuery` hook to subscribe to this query, and the following happens to get an initial value: + +* The Convex client sends a message to the Convex server to subscribe to the query +* The Convex server runs the function, which reads data from the database +* The Convex server sends a message to the client with the function's result + +In this case the initial result looks like this (1): + +``` +[ + { _id: "e4g", title: "Grocery shopping", complete: false }, + { _id: "u9v", title: "Plant new flowers", complete: false }, +]; +``` + +Then you use a mutation to mark an item as completed (2). Convex then reruns the query (3) to get an updated result. And pushes the result to the web app via the WebSocket connection (4): + +``` +[ + { _id: "e4g", title: "Grocery shopping", complete: false }, +]; +``` + +## Beyond reactivity[​](#beyond-reactivity "Direct link to Beyond reactivity") + +Beyond reactivity, Convex's architecture is crucial for a deeper reason. Convex does not let your app have inconsistent state at any layer of the stack. + +To illustrate this, let's imagine you're building a shopping cart for an e-commerce store. + +![Convex in your app](/assets/images/convex-swaghaus-dcc9919685db6a7f34378afc500f68cd.png) + +On the product listing page, you have two numbers, one showing the number of items remaining in stock and another showing the number of items in your shopping cart. Each number is a result of a different query function. + +Every time you press the "Add to Cart" button, a mutation is called to remove one item from the stock and add it to the shopping cart. + +The mutation to change the cart runs in a transaction, so your database is always in a consistent state. The reactive database knows that the queries showing the number of items in stock and the number of items in the shopping cart both need to be updated. The queries are invalidated and rerun. The results are pushed to the web app via the WebSocket connection. + +The client library makes sure that both queries update at the same time in the web app since they reflect a singular moment in time in your database. You never have a moment where those numbers don't add up. Your app always shows consistent data. + +You can see this example in action in the [Swaghaus sample app](https://swaghaus.biz/). + +## For human and AI generated code[​](#for-human-and-ai-generated-code "Direct link to For human and AI generated code") + +Convex is designed around a small set of composable abstractions with strong guarantees that result in code that is not only faster to write, it’s easier to read and maintain, whether written by a team member or an LLM. Key features make sure you get bug-free AI generated code: + +1. **Queries are Just TypeScript** Your database queries are pure TypeScript functions with end-to-end type safety and IDE support. This means AI can generate database code using the large training set of TypeScript code without switching to SQL. +2. **Less Code for the Same Work** Since so much infrastructure and boiler plate is automatically managed by Convex there is less code to write, and thus less code to get wrong. +3. **Automatic Reactivity** The reactive system automatically tracks data dependencies and updates your UI. AI doesn't need to manually manage subscriptions, WebSocket connections, or complex state synchronization—Convex handles all of this automatically. +4. **Transactional Guarantees** Queries are read-only and mutations run in transactions. These constraints make it nearly impossible for AI to write code that could corrupt your data or leave your app in an inconsistent state. + +Together, these features mean AI can focus on your business logic while Convex's guarantees prevent common failure modes. + +## Learn more[​](#learn-more "Direct link to Learn more") + +If you are intrigued about the details of how Convex pulls this all off, you can read Convex co-founder Sujay's excellent [How Convex Works](https://stack.convex.dev/how-convex-works) blog post. + +Now that you have a good sense of how Convex fits in your app. Let's walk through the overall workflow of setting up and launching a Convex app. diff --git a/.claude/docs/references/trpc/client/disabling-queries.md b/.claude/docs/references/trpc/client/disabling-queries.md new file mode 100644 index 00000000..c4221bd3 --- /dev/null +++ b/.claude/docs/references/trpc/client/disabling-queries.md @@ -0,0 +1,26 @@ +--- +id: disabling-queries +title: Disabling Queries +sidebar_label: Disabling Queries +slug: /client/react/disabling-queries +--- + +To disable queries, you can pass `skipToken` as the first argument to `useQuery` or `useInfiniteQuery`. This will prevent the query from being executed. + +### Typesafe conditional queries using `skipToken` + +```tsx +import { skipToken } from '@tanstack/react-query'; + + +export function MyComponent() { + +const [name, setName] = useState(); + +const result = trpc.getUserByName.useQuery(name ? { name: name } : skipToken); + + return ( + ... + ) +} +``` diff --git a/.claude/docs/references/trpc/client/infer-types.md b/.claude/docs/references/trpc/client/infer-types.md new file mode 100644 index 00000000..c3560ded --- /dev/null +++ b/.claude/docs/references/trpc/client/infer-types.md @@ -0,0 +1,219 @@ +--- +id: infer-types +title: Inferring Types +sidebar_label: Inferring Types +slug: /client/react/infer-types +--- + + + +```twoslash include server +// @module: esnext +// @filename: server.ts +import { initTRPC } from '@trpc/server'; +import { z } from "zod"; + +const t = initTRPC.create(); + +const appRouter = t.router({ + post: t.router({ + list: t.procedure + .query(() => { + // imaginary db call + return [{ id: 1, title: 'tRPC is the best!' }]; + }), + byId: t.procedure + .input(z.string()) + .query(({ input }) => { + // imaginary db call + return { id: 1, title: 'tRPC is the best!' }; + }), + create: t.procedure + .input(z.object({ title: z.string(), text: z.string(), })) + .mutation(({ input }) => { + // imaginary db call + return { id: 1, ...input }; + }), + }), +}); + +export type AppRouter = typeof appRouter; +``` + +In addition to the type inference made available by `@trpc/server` ([see here](/docs/client/vanilla/infer-types)) this integration also provides some inference helpers for usage purely in React. + +## Infer React Query options based on your router + +When creating custom hooks around tRPC procedures, it's sometimes necessary to have the types of the options inferred from the router. You can do so via the `inferReactQueryProcedureOptions` helper exported from `@trpc/react-query`. + +```ts twoslash title='trpc.ts' +// @module: esnext +// @include: server +// @filename: trpc.ts +// ---cut--- +import { + createTRPCReact, + type inferReactQueryProcedureOptions, +} from '@trpc/react-query'; +import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; +import type { AppRouter } from './server'; + +// infer the types for your router +export type ReactQueryOptions = inferReactQueryProcedureOptions; +export type RouterInputs = inferRouterInputs; +export type RouterOutputs = inferRouterOutputs; + +export const trpc = createTRPCReact(); +``` + +```ts twoslash title='usePostCreate.ts' +// @module: esnext +// @include: server +// @filename: usePostCreate.ts +// @noErrors +// ---cut--- +import { + trpc, + type ReactQueryOptions, + type RouterInputs, + type RouterOutputs, +} from './trpc'; + +type PostCreateOptions = ReactQueryOptions['post']['create']; + +function usePostCreate(options?: PostCreateOptions) { + const utils = trpc.useUtils(); + + return trpc.post.create.useMutation({ + ...options, + onSuccess(post) { + // invalidate all queries on the post router + // when a new post is created + utils.post.invalidate(); + options?.onSuccess?.(post); + }, + }); +} +``` + +```ts twoslash title='usePostById.ts' +// @module: esnext +// @include: server +// @filename: usePostById.ts +// @noErrors +// ---cut--- +import { ReactQueryOptions, RouterInputs, trpc } from './trpc'; + +type PostByIdOptions = ReactQueryOptions['post']['byId']; +type PostByIdInput = RouterInputs['post']['byId']; + +function usePostById(input: PostByIdInput, options?: PostByIdOptions) { + return trpc.post.byId.useQuery(input, options); +} +``` + +## Infer abstract types from a "Router Factory" + +If you write a factory which creates a similar router interface several times in your application, you may wish to share client code between usages of the factory. `@trpc/react-query/shared` exports several types which can be used to generate abstract types for a router factory, and build common React components which are passed the router as a prop. + +```tsx twoslash title='api/factory.ts' +// @module: esnext +// @include: server +// @noErrors +// ---cut--- + +import { t, publicProcedure } from './trpc'; + +// @trpc/react-query/shared exports several **Like types which can be used to generate abstract types +import { RouterLike, UtilsLike } from '@trpc/react-query/shared'; + +// Factory function written by you, however you need, +// so long as you can infer the resulting type of t.router() later +export function createMyRouter() { + return t.router({ + createThing: publicProcedure + .input(ThingRequest) + .output(Thing) + .mutation(/* do work */), + listThings: publicProcedure + .input(ThingQuery) + .output(ThingArray) + .query(/* do work */), + }) +} + +// Infer the type of your router, and then generate the abstract types for use in the client +type MyRouterType = ReturnType +export MyRouterLike = RouterLike +export MyRouterUtilsLike = UtilsLike +``` + +```tsx twoslash title='api/server.ts' +// @module: esnext +// @include: server +// @noErrors +// ---cut--- + +export type AppRouter = typeof appRouter; + +// Export your MyRouter types to the client +export type { MyRouterLike, MyRouterUtilsLike } from './factory'; +``` + +```tsx twoslash title='frontend/usePostCreate.ts' +// @module: esnext +// @include: server +// @noErrors +// ---cut--- +import type { MyRouterLike, MyRouterUtilsLike, trpc, useUtils } from './trpc'; + +type MyGenericComponentProps = { + route: MyRouterLike; + utils: MyRouterUtilsLike; +}; + +function MyGenericComponent(props: MyGenericComponentProps) { + const { route } = props; + const thing = route.listThings.useQuery({ + filter: 'qwerty', + }); + + const mutation = route.doThing.useMutation({ + onSuccess() { + props.utils.listThings.invalidate(); + }, + }); + + function handleClick() { + mutation.mutate({ + name: 'Thing 1', + }); + } + + return; /* ui */ +} + +function MyPageComponent() { + const utils = useUtils(); + + return ( + + ); +} + +function MyOtherPageComponent() { + const utils = useUtils(); + + return ( + + ); +} +``` + +A more complete working example [can be found here](https://github.com/trpc/trpc/tree/main/packages/tests/server/react/polymorphism.test.tsx) diff --git a/.claude/docs/references/trpc/client/overview.md b/.claude/docs/references/trpc/client/overview.md new file mode 100644 index 00000000..7233d438 --- /dev/null +++ b/.claude/docs/references/trpc/client/overview.md @@ -0,0 +1,16 @@ +--- +id: overview +title: Client Overview +sidebar_label: Overview +slug: /client +--- + +While a tRPC API can be called using normal HTTP requests like any other REST API, you will need a **client** to benefit from tRPC's typesafety. + +A client knows the procedures that are available in your API, and their inputs and outputs. It uses this information to give you autocomplete on your queries and mutations, correctly type the returned data, and show errors if you are writing requests that don't match the shape of your backend. + +If you are using React, the best way to call a tRPC API is by using our [React Query Integration](./react/introduction.mdx), which in addition to typesafe API calls also offers caching, invalidation, and management of loading and error state. If you are using Next.js with the `/pages` directory, you can use our [Next.js integration](./nextjs/introduction.mdx), which adds helpers for Serverside Rendering and Static Generation in addition to the React Query Integration. + +If you want to call a tRPC API from another server or from a frontend framework for which we don't have an integration, you can use the [Vanilla Client](./vanilla/introduction.md). + +In addition to the React and Next.js integrations and the Vanilla Client, there are a variety of [community-built integrations for a variety of other frameworks](/docs/community/awesome-trpc#frontend-frameworks). Please note that these are not maintained by the tRPC team. diff --git a/.claude/docs/references/trpc/client/server-components.mdx b/.claude/docs/references/trpc/client/server-components.mdx new file mode 100644 index 00000000..29b94091 --- /dev/null +++ b/.claude/docs/references/trpc/client/server-components.mdx @@ -0,0 +1,455 @@ +--- +id: server-components +title: Set up with React Server Components +sidebar_label: Server Components +slug: /client/tanstack-react-query/server-components +--- + +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +This guide is an overview of how one may use tRPC with a React Server Components (RSC) framework such as Next.js App Router. +Be aware that RSC on its own solves a lot of the same problems tRPC was designed to solve, so you may not need tRPC at all. + +There are also not a one-size-fits-all way to integrate tRPC with RSCs, so see this guide as a starting point and adjust it +to your needs and preferences. + +:::info +If you're looking for how to use tRPC with Server Actions, check out [this blog post by Julius](/blog/trpc-actions). +::: + +:::caution +Please read React Query's [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr) docs before proceeding to understand the different types of server rendering +and what footguns to avoid. +::: + +## Add tRPC to existing projects + +### 1. Install deps + +import { InstallSnippet } from '@site/src/components/InstallSnippet'; + + + +### 2. Create a tRPC router + +Initialize your tRPC backend in `trpc/init.ts` using the `initTRPC` function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to the [Quickstart guide](/docs/quickstart) and [Backend usage docs](/docs/server/introduction) for tRPC information. + +:::info +The file names used here are not enforced by tRPC. You may use any file structure you wish. +::: + +
+View sample backend + +```ts title='trpc/init.ts' +import { initTRPC } from '@trpc/server'; +import { cache } from 'react'; + +export const createTRPCContext = cache(async () => { + /** + * @see: https://trpc.io/docs/server/context + */ + return { userId: 'user_123' }; +}); + +// Avoid exporting the entire t-object +// since it's not very descriptive. +// For instance, the use of a t variable +// is common in i18n libraries. +const t = initTRPC.create({ + /** + * @see https://trpc.io/docs/server/data-transformers + */ + // transformer: superjson, +}); + +// Base router and procedure helpers +export const createTRPCRouter = t.router; +export const createCallerFactory = t.createCallerFactory; +export const baseProcedure = t.procedure; +``` + +
+ +```ts title='trpc/routers/_app.ts' +import { z } from 'zod'; +import { baseProcedure, createTRPCRouter } from '../init'; + +export const appRouter = createTRPCRouter({ + hello: baseProcedure + .input( + z.object({ + text: z.string(), + }), + ) + .query((opts) => { + return { + greeting: `hello ${opts.input.text}`, + }; + }), +}); + +// export type definition of API +export type AppRouter = typeof appRouter; +``` + +
+ +:::note +The backend adapter depends on your framework and how it sets up API routes. The following example sets up GET and POST routes at `/api/trpc/*` using the [fetch adapter](https://trpc.io/docs/server/adapters/fetch) in Next.js. +::: + +```ts title='app/api/trpc/[trpc]/route.ts' +import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; +import { createTRPCContext } from '~/trpc/init'; +import { appRouter } from '~/trpc/routers/_app'; + +const handler = (req: Request) => + fetchRequestHandler({ + endpoint: '/api/trpc', + req, + router: appRouter, + createContext: createTRPCContext, + }); + +export { handler as GET, handler as POST }; +``` + +
+ +### 3. Create a Query Client factory + +Create a shared file `trpc/query-client.ts` that exports a function that creates a `QueryClient` instance. + +```ts title='trpc/query-client.ts' +import { + defaultShouldDehydrateQuery, + QueryClient, +} from '@tanstack/react-query'; +import superjson from 'superjson'; + +export function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30 * 1000, + }, + dehydrate: { + // serializeData: superjson.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + }, + hydrate: { + // deserializeData: superjson.deserialize, + }, + }, + }); +} +``` + +We're setting a few default options here: + +- `staleTime`: With SSR, we usually want to set some default staleTime above 0 to avoid refetching immediately on the client. +- `shouldDehydrateQuery`: This is a function that determines whether a query should be dehydrated or not. Since the RSC transport protocol + supports hydrating promises over the network, we extend the `defaultShouldDehydrateQuery` function to also include queries that are + still pending. This will allow us to start prefetching in a server component high up the tree, then consuming that promise in a client component further down. +- `serializeData` and `deserializeData` (optional): If you set up a [data transformer](https://trpc.io/docs/server/data-transformers) in the previous step, set this option + to make sure the data is serialized correctly when hydrating the query client over the server-client boundary. + +### 4. Create a tRPC client for Client Components + +The `trpc/client.tsx` is the entrypoint when consuming your tRPC API from client components. In here, import the **type definition** of +your tRPC router and create typesafe hooks using `createTRPCContext`. We'll also export our context provider from this file. + +```tsx title='trpc/client.tsx' +'use client'; + +// ^-- to make sure we can mount the Provider from a server component +import type { QueryClient } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import { createTRPCContext } from '@trpc/tanstack-react-query'; +import { useState } from 'react'; +import { makeQueryClient } from './query-client'; +import type { AppRouter } from './routers/_app'; + +export const { TRPCProvider, useTRPC } = createTRPCContext(); + +let browserQueryClient: QueryClient; +function getQueryClient() { + if (typeof window === 'undefined') { + // Server: always make a new query client + return makeQueryClient(); + } + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; +} + +function getUrl() { + const base = (() => { + if (typeof window !== 'undefined') return ''; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return 'http://localhost:3000'; + })(); + return `${base}/api/trpc`; +} + +export function TRPCReactProvider( + props: Readonly<{ + children: React.ReactNode; + }>, +) { + // NOTE: Avoid useState when initializing the query client if you don't + // have a suspense boundary between this and the code that may + // suspend because React will throw away the client on the initial + // render if it suspends and there is no boundary + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + httpBatchLink({ + // transformer: superjson, <-- if you use a data transformer + url: getUrl(), + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); +} +``` + +Mount the provider in the root of your application (e.g. `app/layout.tsx` when using Next.js). + +### 5. Create a tRPC caller for Server Components + +To prefetch queries from server components, we create a proxy from our router. You can also pass in a client if your router is on a separate server. + +```tsx title='trpc/server.tsx' +import 'server-only'; // <-- ensure this file cannot be imported from the client + +import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; +import { cache } from 'react'; +import { createTRPCContext } from './init'; +import { makeQueryClient } from './query-client'; +import { appRouter } from './routers/_app'; + +// IMPORTANT: Create a stable getter for the query client that +// will return the same client during the same request. +export const getQueryClient = cache(makeQueryClient); + +export const trpc = createTRPCOptionsProxy({ + ctx: createTRPCContext, + router: appRouter, + queryClient: getQueryClient, +}); + +// If your router is on a separate server, pass a client: +createTRPCOptionsProxy({ + client: createTRPCClient({ + links: [httpLink({ url: '...' })], + }), + queryClient: getQueryClient, +}); +``` + +## Using your API + +Now you can use your tRPC API in your app. While you can use the React Query hooks in client components just like you would in any other React app, +we can take advantage of the RSC capabilities by prefetching queries in a server component high up the tree. You may be familiar with this +concept as "render as you fetch" commonly implemented as loaders. This means the request fires as soon as possible but without suspending until +the data is needed by using the `useQuery` or `useSuspenseQuery` hooks. + +This approach leverages Next.js App Router's streaming capabilities, initiating the query on the server and streaming data to the client as it becomes available. +It optimizes both the time to first byte in the browser and the data fetch time, resulting in faster page loads. +However, `greeting.data` may initially be `undefined` before the data streams in. + +If you prefer to avoid this initial undefined state, you can `await` the `prefetchQuery` call. +This ensures the query on the client always has data on first render, but it comes with a tradeoff - +the page will load more slowly since the server must complete the query before sending HTML to the client. + +```tsx title='app/page.tsx' +import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; +import { getQueryClient, trpc } from '~/trpc/server'; +import { ClientGreeting } from './client-greeting'; + +export default async function Home() { + const queryClient = getQueryClient(); + void queryClient.prefetchQuery( + trpc.hello.queryOptions({ + /** input */ + }), + ); + + return ( + +
...
+ {/** ... */} + +
+ ); +} +``` + +```tsx title='app/client-greeting.tsx' +'use client'; + +// <-- hooks can only be used in client components +import { useQuery } from '@tanstack/react-query'; +import { useTRPC } from '~/trpc/client'; + +export function ClientGreeting() { + const trpc = useTRPC(); + const greeting = useQuery(trpc.hello.queryOptions({ text: 'world' })); + if (!greeting.data) return
Loading...
; + return
{greeting.data.greeting}
; +} +``` + +:::tip +You can also create a `prefetch` and `HydrateClient` helper functions to make it a bit more consice and reusable: + +```tsx title='trpc/server.tsx' +export function HydrateClient(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + return ( + + {props.children} + + ); +} + +export function prefetch>>( + queryOptions: T, +) { + const queryClient = getQueryClient(); + if (queryOptions.queryKey[1]?.type === 'infinite') { + void queryClient.prefetchInfiniteQuery(queryOptions as any); + } else { + void queryClient.prefetchQuery(queryOptions); + } +} +``` + +Then you can use it like this: + +```tsx +import { HydrateClient, prefetch, trpc } from '~/trpc/server'; + +function Home() { + prefetch( + trpc.hello.queryOptions({ + /** input */ + }), + ); + + return ( + +
...
+ {/** ... */} + +
+ ); +} +``` + +::: + +### Leveraging Suspense + +You may prefer handling loading and error states using Suspense and Error Boundaries. You can do this by using the `useSuspenseQuery` hook. + +```tsx title='app/page.tsx' +import { HydrateClient, prefetch, trpc } from '~/trpc/server'; +import { Suspense } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ClientGreeting } from './client-greeting'; + +export default async function Home() { + prefetch(trpc.hello.queryOptions()); + + return ( + +
...
+ {/** ... */} + Something went wrong}> + Loading...}> + + + +
+ ); +} +``` + +```tsx title='app/client-greeting.tsx' +'use client'; + +import { useSuspenseQuery } from '@tanstack/react-query'; +import { trpc } from '~/trpc/client'; + +export function ClientGreeting() { + const trpc = useTRPC(); + const { data } = useSuspenseQuery(trpc.hello.queryOptions()); + return
{data.greeting}
; +} +``` + +### Getting data in a server component + +If you need access to the data in a server component, we recommend creating a server caller and using it directly. Please note that this method is detached from your query client and does not +store the data in the cache. This means that you cannot use the data in a server component and expect it to be available in the client. This is +intentional and explained in more detail in the [Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) +guide. + +```tsx title='trpc/server.tsx' +// ... +export const caller = appRouter.createCaller(createTRPCContext); +``` + +```tsx title='app/page.tsx' +import { caller } from '~/trpc/server'; + +export default async function Home() { + const greeting = await caller.hello(); + // ^? { greeting: string } + + return
{greeting.greeting}
; +} +``` + +If you **really** need to use the data both on the server as well as inside client components and understand the tradeoffs explained in the +[Advanced Server Rendering](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#data-ownership-and-revalidation) +guide, you can use `fetchQuery` instead of `prefetch` to have the data both on the server as well as hydrating it down to the client: + +```tsx title='app/page.tsx' +import { getQueryClient, HydrateClient, trpc } from '~/trpc/server'; + +export default async function Home() { + const queryClient = getQueryClient(); + const greeting = await queryClient.fetchQuery(trpc.hello.queryOptions()); + + // Do something with greeting on the server + + return ( + +
...
+ {/** ... */} + +
+ ); +} +``` diff --git a/.claude/docs/references/trpc/client/setup.mdx b/.claude/docs/references/trpc/client/setup.mdx new file mode 100644 index 00000000..31ec9274 --- /dev/null +++ b/.claude/docs/references/trpc/client/setup.mdx @@ -0,0 +1,215 @@ +--- +id: setup +title: TanStack React Query +sidebar_label: Setup +description: TanStack React Query setup +slug: /client/tanstack-react-query/setup +--- + +Compared to our [classic React Query Integration](/docs/client/react) this client is simpler and more TanStack Query-native, providing factories for common TanStack React Query interfaces like QueryKeys, QueryOptions, and MutationOptions. We think it's the future and recommend using this over the classic client, read the announcement post for more information about this change. + +:::tip +You can try this integration out on the homepage of tRPC.io: [https://trpc.io/?try=minimal-react#try-it-out](/?try=minimal-react#try-it-out) +::: + +
+❓ Do I have to use an integration? + +No! The integration is fully optional. You can use `@tanstack/react-query` using just a [vanilla tRPC client](/docs/client/vanilla), although then you'll have to manually manage query keys and do not get the same level of DX as when using the integration package. + +```ts title='utils/trpc.ts' +export const trpc = createTRPCClient({ + links: [httpBatchLink({ url: 'YOUR_API_URL' })], +}); +``` + +```tsx title='components/PostList.tsx' +function PostList() { + const { data } = useQuery({ + queryKey: ['posts'], + queryFn: () => trpc.post.list.query(), + }); + data; // Post[] + + // ... +} +``` + +
+ +## Setup + +### 1. Install dependencies + +The following dependencies should be installed + +import { InstallSnippet } from '@site/src/components/InstallSnippet'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + + + +### 2. Import your `AppRouter` + +```twoslash include router +// @filename: server/router.ts +import { initTRPC } from '@trpc/server'; +import { z } from "zod"; +const t = initTRPC.create(); + +const appRouter = t.router({ + getUser: t.procedure.input(z.object({ id: z.string() })).query(() => ({ name: 'foo' })), + createUser: t.procedure.input(z.object({ name: z.string() })).mutation(() => 'bar'), +}); +export type AppRouter = typeof appRouter; +``` + +```twoslash include utils-a +// @filename: utils/trpc.ts +// ---cut--- +import { createTRPCContext } from '@trpc/tanstack-react-query'; +import type { AppRouter } from '../server/router'; + +export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext(); +``` + +import ImportAppRouter from '../../partials/_import-approuter.mdx'; + + + +### 3a. Set up the tRPC context provider + +In cases where you rely on React context, such as when using server-side rendering in full-stack frameworks like Next.js, it's important to create a new QueryClient for each request so that your users don't end up sharing the same cache, you can use the `createTRPCContext` to create a set of type-safe context providers and consumers from your `AppRouter` type signature. + +```tsx title='utils/trpc.ts' twoslash +// @include: router +// @include: utils-a +``` + +Then, create a tRPC client, and wrap your application in the `TRPCProvider`, as below. You will also need to set up and connect React Query, which [they document in more depth](https://tanstack.com/query/latest/docs/framework/react/quick-start). + +:::tip +If you already use React Query in your application, you **should** re-use the `QueryClient` and `QueryClientProvider` you already have. You can read more about the QueryClient initialization in the [React Query docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#initial-setup). +::: + +```tsx title='components/App.tsx' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import { useState } from 'react'; +import { TRPCProvider } from './utils/trpc'; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined = undefined; + +function getQueryClient() { + if (typeof window === 'undefined') { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } +} + +export function App() { + const queryClient = getQueryClient(); + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + httpBatchLink({ + url: 'http://localhost:2022', + }), + ], + }), + ); + + return ( + + + {/* Your app here */} + + + ); +} +``` + +### 3b. Set up with Query/Mutation Key Prefixing enabled + +If you want to prefix all queries and mutations with a specific key, see [Query Key Prefixing](./usage.mdx#keyPrefix) for setup and usage examples. + +### 3c. Set up without React context + +When building an SPA using only client-side rendering with something like Vite, you can create the `QueryClient` and tRPC client outside of React context as singletons. + +```ts title='utils/trpc.ts' +import { QueryClient } from '@tanstack/react-query'; +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; +import type { AppRouter } from '../server/router'; + +export const queryClient = new QueryClient(); + +const trpcClient = createTRPCClient({ + links: [httpBatchLink({ url: 'http://localhost:2022' })], +}); + +export const trpc = createTRPCOptionsProxy({ + client: trpcClient, + queryClient, +}); +``` + +```tsx title='components/App.tsx' +import { QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { queryClient } from './utils/trpc'; + +export function App() { + return ( + + {/* Your app here */} + + ); +} +``` + +### 4. Fetch data + +You can now use the tRPC React Query integration to call queries and mutations on your API. + +```tsx title='components/user-list.tsx' +import { useMutation, useQuery } from '@tanstack/react-query'; +import { useTRPC } from '../utils/trpc'; + +export default function UserList() { + const trpc = useTRPC(); // use `import { trpc } from './utils/trpc'` if you're using the singleton pattern + + const userQuery = useQuery(trpc.getUser.queryOptions({ id: 'id_bilbo' })); + const userCreator = useMutation(trpc.createUser.mutationOptions()); + + return ( +
+

{userQuery.data?.name}

+ + +
+ ); +} +``` diff --git a/.claude/docs/references/trpc/client/usage.mdx b/.claude/docs/references/trpc/client/usage.mdx new file mode 100644 index 00000000..eec631ba --- /dev/null +++ b/.claude/docs/references/trpc/client/usage.mdx @@ -0,0 +1,469 @@ +--- +id: usage +title: TanStack React Query +sidebar_label: Usage +description: TanStack React Query usage +slug: /client/tanstack-react-query/usage +--- + +Compared to our [classic React Query Integration](/docs/client/react) this client is simpler and more TanStack Query-native, providing factories for common TanStack React Query interfaces like QueryKeys, QueryOptions, and MutationOptions. We think it's the future and recommend using this over the classic client, read the announcement post for more information about this change. + +## Quick example query + +```tsx +import { useQuery } from '@tanstack/react-query'; +import { useTRPC } from './trpc'; + +function Users() { + const trpc = useTRPC(); + + const greetingQuery = useQuery(trpc.greeting.queryOptions({ name: 'Jerry' })); + + // greetingQuery.data === 'Hello Jerry' +} +``` + +## Usage + +The philosophy of this client is to provide thin and type-safe factories which work natively and type-safely with Tanstack React Query. This means just by following the autocompletes the client gives you, you can focus on building just with the knowledge the [TanStack React Query docs](https://tanstack.com/query/latest/docs/framework/react/overview) provide. + +```tsx +export default function Basics() { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + + // Create QueryOptions which can be passed to query hooks + const myQueryOptions = trpc.path.to.query.queryOptions({ /** inputs */ }) + const myQuery = useQuery(myQueryOptions) + // or: + // useSuspenseQuery(myQueryOptions) + // useInfiniteQuery(myQueryOptions) + + // Create MutationOptions which can be passed to useMutation + const myMutationOptions = trpc.path.to.mutation.mutationOptions() + const myMutation = useMutation(myMutationOptions) + + // Create a QueryKey which can be used to manipulated many methods + // on TanStack's QueryClient in a type-safe manner + const myQueryKey = trpc.path.to.query.queryKey() + + const invalidateMyQueryKey = () => { + queryClient.invalidateQueries({ queryKey: myQueryKey }) + } + + return ( + // Your app here + ) +} +``` + +The `trpc` object is fully type-safe and will provide autocompletes for all the procedures in your `AppRouter`. At the end of the proxy, the following methods are available: + +### `queryOptions` - querying data {#queryOptions} + +Available for all query procedures. Provides a type-safe wrapper around [Tanstack's `queryOptions` function](https://tanstack.com/query/latest/docs/framework/react/reference/queryOptions). The first argument is the input for the procedure, and the second argument accepts any native Tanstack React Query options. + +```ts +const queryOptions = trpc.path.to.query.queryOptions( + { + /** input */ + }, + { + // Any Tanstack React Query options + staleTime: 1000, + }, +); +``` + +You can additionally provide a `trpc` object to the `queryOptions` function to provide tRPC request options to the client. + +```ts +const queryOptions = trpc.path.to.query.queryOptions( + { + /** input */ + }, + { + trpc: { + // Provide tRPC request options to the client + context: { + // see https://trpc.io/docs/client/links#managing-context + }, + }, + }, +); +``` + +If you want to disable a query in a type safe way, you can use `skipToken`: + +```ts +import { skipToken } from '@tanstack/react-query'; + +const query = useQuery( + trpc.user.details.queryOptions( + user?.id && project?.id + ? { + userId: user.id, + projectId: project.id, + } + : skipToken, + { + staleTime: 1000, + }, + ), +); +``` + +The result can be passed to `useQuery` or `useSuspenseQuery` hooks or query client methods like `fetchQuery`, `prefetchQuery`, `prefetchInfiniteQuery`, `invalidateQueries`, etc. + +### `infiniteQueryOptions` - querying infinite data {#infiniteQueryOptions} + +Available for all query procedures that takes a cursor input. Provides a type-safe wrapper around [Tanstack's `infiniteQueryOptions` function](https://tanstack.com/query/latest/docs/framework/react/reference/infiniteQueryOptions). The first argument is the input for the procedure, and the second argument accepts any native Tanstack React Query options. + +```ts +const infiniteQueryOptions = trpc.path.to.query.infiniteQueryOptions( + { + /** input */ + }, + { + // Any Tanstack React Query options + getNextPageParam: (lastPage, pages) => lastPage.nextCursor, + }, +); +``` + +### `queryKey` - getting the query key and performing operations on the query client {#queryKey} + +Available for all query procedures. Allows you to access the query key in a type-safe manner. + +```ts +const queryKey = trpc.path.to.query.queryKey(); +``` + +Since Tanstack React Query uses fuzzy matching for query keys, you can also create a partial query key for any sub-path to match all queries belonging to a router: + +```ts +const queryKey = trpc.router.pathKey(); +``` + +Or even the root path to match all tRPC queries: + +```ts +const queryKey = trpc.pathKey(); +``` + +### `infiniteQueryKey` - getting the infinite query key {#infiniteQueryKey} + +Available for all query procedures that takes a cursor input. Allows you to access the query key for an infinite query in a type-safe manner. + +```ts +const infiniteQueryKey = trpc.path.to.query.infiniteQueryKey({ + /** input */ +}); +``` + +The result can be used with query client methods like `getQueryData`, `setQueryData`, `invalidateQueries`, etc. + +```ts +const queryClient = useQueryClient(); + +// Get cached data for an infinite query +const cachedData = queryClient.getQueryData( + trpc.path.to.query.infiniteQueryKey({ cursor: 0 }), +); + +// Set cached data for an infinite query +queryClient.setQueryData( + trpc.path.to.query.infiniteQueryKey({ cursor: 0 }), + (data) => { + // Modify the data + return data; + }, +); +``` + +### `queryFilter` - creating query filters {#queryFilter} + +Available for all query procedures. Allows creating [query filters](https://tanstack.com/query/latest/docs/framework/react/guides/filters#query-filters) in a type-safe manner. + +```ts +const queryFilter = trpc.path.to.query.queryFilter( + { + /** input */ + }, + { + // Any Tanstack React Query filter + predicate: (query) => { + query.state.data; + }, + }, +); +``` + +Like with query keys, if you want to run a filter across a whole router you can use `pathFilter` to target any sub-path. + +```ts +const queryFilter = trpc.path.pathFilter({ + // Any Tanstack React Query filter + predicate: (query) => { + query.state.data; + }, +}); +``` + +Useful for creating filters that can be passed to client methods like `queryClient.invalidateQueries` etc. + +### `infiniteQueryFilter` - creating infinite query filters {#infiniteQueryFilter} + +Available for all query procedures that takes a cursor input. Allows creating [query filters](https://tanstack.com/query/latest/docs/framework/react/guides/filters#query-filters) for infinite queries in a type-safe manner. + +```ts +const infiniteQueryFilter = trpc.path.to.query.infiniteQueryFilter( + { + /** input */ + }, + { + // Any Tanstack React Query filter + predicate: (query) => { + query.state.data; + }, + }, +); +``` + +Useful for creating filters that can be passed to client methods like `queryClient.invalidateQueries` etc. + +```ts +await queryClient.invalidateQueries( + trpc.path.to.query.infiniteQueryFilter( + {}, + { + predicate: (query) => { + // Filter logic based on query state + return query.state.data?.pages.length > 0; + }, + }, + ), +); +``` + +### `mutationOptions` - creating mutation options {#mutationOptions} + +Available for all mutation procedures. Provides a type-safe identify function for constructing options that can be passed to `useMutation`. + +```ts +const mutationOptions = trpc.path.to.mutation.mutationOptions({ + // Any Tanstack React Query options + onSuccess: (data) => { + // do something with the data + }, +}); +``` + +### `mutationKey` - getting the mutation key {#mutationKey} + +Available for all mutation procedures. Allows you to get the mutation key in a type-safe manner. + +```ts +const mutationKey = trpc.path.to.mutation.mutationKey(); +``` + +### `subscriptionOptions` - creating subscription options {#subscriptionOptions} + +TanStack does not provide a subscription hook, so we continue to expose our own abstraction here which works with a [standard tRPC subscription setup](/docs/server/subscriptions). +Available for all subscription procedures. Provides a type-safe identify function for constructing options that can be passed to `useSubscription`. +Note that you need to have either the [`httpSubscriptionLink`](/docs/client/links/httpSubscriptionLink) or [`wsLink`](/docs/client/links/wsLink) configured in your tRPC client to use subscriptions. + +```tsx +function SubscriptionExample() { + const trpc = useTRPC(); + const subscription = useSubscription( + trpc.path.to.subscription.subscriptionOptions( + { + /** input */ + }, + { + enabled: true, + onStarted: () => { + // do something when the subscription is started + }, + onData: (data) => { + // you can handle the data here + }, + onError: (error) => { + // you can handle the error here + }, + onConnectionStateChange: (state) => { + // you can handle the connection state here + }, + }, + ), + ); + + // Or you can handle the state here + subscription.data; // The lastly received data + subscription.error; // The lastly received error + + /** + * The current status of the subscription. + * Will be one of: `'idle'`, `'connecting'`, `'pending'`, or `'error'`. + * + * - `idle`: subscription is disabled or ended + * - `connecting`: trying to establish a connection + * - `pending`: connected to the server, receiving data + * - `error`: an error occurred and the subscription is stopped + */ + subscription.status; + + // Reset the subscription (if you have an error etc) + subscription.reset(); + + return <>{/* ... */}; +} +``` + +### Query Key Prefixing {#keyPrefix} + +When using multiple tRPC providers in a single application (e.g., connecting to different backend services), queries with the same path will collide in the cache. You can prevent this by enabling query key prefixing. + +```tsx +// Without prefixes - these would collide! +const authQuery = useQuery(trpcAuth.list.queryOptions()); // auth service +const billingQuery = useQuery(trpcBilling.list.queryOptions()); // billing service +``` + +Enable the feature flag when creating your context: + +```tsx title='utils/trpc.ts' +// [...] + +const billing = createTRPCContext(); +export const BillingProvider = billing.TRPCProvider; +export const useBilling = billing.useTRPC; +export const createBillingClient = () => + createTRPCClient({ + links: [ + /* ... */ + ], + }); + +const account = createTRPCContext(); +export const AccountProvider = account.TRPCProvider; +export const useAccount = account.useTRPC; +export const createAccountClient = () => + createTRPCClient({ + links: [ + /* ... */ + ], + }); +``` + +```tsx title='App.tsx' +// [...] + +export function App() { + const [queryClient] = useState(() => new QueryClient()); + const [billingClient] = useState(() => createBillingClient()); + const [accountClient] = useState(() => createAccountClient()); + + return ( + + + + {/* ... */} + + + + ); +} +``` + +```tsx title='components/MyComponent.tsx' +// [...] + +export function MyComponent() { + const billing = useBilling(); + const account = useAccount(); + + const billingList = useQuery(billing.list.queryOptions()); + const accountList = useQuery(account.list.queryOptions()); + + return ( +
+
Billing: {JSON.stringify(billingList.data ?? null)}
+
Account: {JSON.stringify(accountList.data ?? null)}
+
+ ); +} +``` + +The query keys will be properly prefixed to avoid collisions: + +```tsx twoslash +// Example of how the query keys look with prefixes +const queryKeys = [ + [['billing'], ['list'], { type: 'query' }], + [['account'], ['list'], { type: 'query' }], +]; +``` + +### Inferring Input and Output types + +When you need to infer the input and output types for a procedure or router, there are 2 options available depending on the situation. + +Infer the input and output types of a full router + +```ts +import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; +import { AppRouter } from './path/to/server'; + +export type Inputs = inferRouterInputs; +export type Outputs = inferRouterOutputs; +``` + +Infer types for a single procedure + +```ts +import type { inferInput, inferOutput } from '@trpc/tanstack-react-query'; + +function Component() { + const trpc = useTRPC(); + + type Input = inferInput; + type Output = inferOutput; +} +``` + +### Accessing the tRPC client {#useTRPCClient} + +If you used the [setup with React Context](/docs/client/tanstack-react-query/setup#3a-setup-the-trpc-context-provider), you can access the tRPC client using the `useTRPCClient` hook. + +```tsx +import { useTRPCClient } from './trpc'; + +function Component() { + const trpcClient = useTRPCClient(); + + const result = await trpcClient.path.to.procedure.query({ + /** input */ + }); +} +``` + +If you [setup without React Context](/docs/client/tanstack-react-query/setup#3b-setup-without-react-context), +you can import the global client instance directly instead. + +```ts +import { client } from './trpc'; + +const result = await client.path.to.procedure.query({ + /** input */ +}); +``` diff --git a/.claude/docs/references/trpc/concepts.mdx b/.claude/docs/references/trpc/concepts.mdx new file mode 100644 index 00000000..01f0c71d --- /dev/null +++ b/.claude/docs/references/trpc/concepts.mdx @@ -0,0 +1,35 @@ +--- +id: concepts +title: Concepts +sidebar_label: Concepts +slug: /concepts +--- + +import { ConceptsChart } from '@site/src/components/ConceptsChart'; + +## What is RPC? What mindset should I adopt? + +### It's just functions + +RPC is short for "Remote Procedure Call". It is a way of calling functions on one computer (the server) from another computer (the client). With traditional HTTP/REST APIs, you call a URL and get a response. With RPC, you call a function and get a response. + +```ts +// HTTP/REST +const res = await fetch('/api/users/1'); +const user = await res.json(); + +// RPC +const user = await api.users.getById({ id: 1 }); +``` + +tRPC (TypeScript Remote Procedure Call) is one implementation of RPC, designed for TypeScript monorepos. It has its own flavor, but is RPC at its heart. + +### Don't think about HTTP/REST implementation details + +If you inspect the network traffic of a tRPC app, you'll see that it's fairly standard HTTP requests and responses, but you don't need to think about the implementation details while writing your application code. You call functions, and tRPC takes care of everything else. You should ignore details like HTTP Verbs, since they carry meaning in REST APIs, but in RPC form part of your function names instead, for instance: `getUser(id)` instead of `GET /users/:id`. + +## Vocabulary + +Below are some terms that are used frequently in the tRPC ecosystem. We'll be using these throughout the documentation, so it's good to get familiar with them. Most of these concepts also have their own pages in the documentation. + + diff --git a/.claude/docs/references/trpc/getting-started.mdx b/.claude/docs/references/trpc/getting-started.mdx new file mode 100644 index 00000000..5b12df67 --- /dev/null +++ b/.claude/docs/references/trpc/getting-started.mdx @@ -0,0 +1,53 @@ +--- +id: getting-started +title: Getting Started +sidebar_label: Getting Started +slug: /getting-started +--- + +## A quick look at tRPC + +For a quick video overview of tRPC's concepts, check out the videos below: + +- [tRPC in 100 seconds](https://www.youtube.com/watch?v=0DyAyLdVW0I) +- [tRPC in 5 minutes](https://www.youtube.com/watch?v=S6rcrkbsDI0) +- [tRPC in 15 minutes](https://www.youtube.com/watch?v=2LYM8gf184U) + +## Give tRPC a try + +The fastest way to try tRPC is in an online REPL. Here are some options you can try out: + +- [Minimal Example](https://stackblitz.com/github/trpc/trpc/tree/main/examples/minimal?file=server%2Findex.ts&file=client%2Findex.ts&view=editor) - a minimal Node.js http server, and a client that calls a function on the server and logs the request to the console. +- [Minimal Next.js Example](https://stackblitz.com/github/trpc/trpc/tree/main/examples/next-minimal-starter?file=src%2Fpages%2Fapi%2Ftrpc%2F[trpc].ts&file=src%2Fpages%2Findex.tsx) - the smallest possible Next.js app that uses tRPC. It has a single endpoint that returns a string, and a page that calls that endpoint and displays the result. + +If you prefer to get started in your local environment, you can use one of our [example apps](./example-apps.mdx) as a starter project that you can experiment with locally. + +## Use tRPC + +"Using tRPC" means different things to different people. The goal of this page is to guide you to the right resources based on your goals. + +### Becoming productive in an existing tRPC project + +- Read the [concepts](./concepts.mdx) page. +- Become familiar with [routers](../server/routers.md), [procedures](../server/procedures.md), [context](../server/context.md), and [middleware](../server/middlewares.md). +- If you are using React, read about [useQuery](../client/react/useQuery.md), [useMutation](../client/react/useMutation.md) and [useUtils](../client/react/useUtils.mdx). + +### Creating a new project + +Since tRPC can live inside of many different frameworks, you will first need to decide where you want to use it. + +On the backend, there are [adapters](../server/adapters-intro.md) for a range of frameworks as well as vanilla Node.js. On the frontend, you can use our [React](../client/react/introduction.mdx) or [Next.js](../client/nextjs/introduction.mdx) integrations, a [third-party integration](../community/awesome-trpc.mdx#frontend-frameworks) for a variety of other frameworks, or the [Vanilla Client](../client/vanilla/setup.mdx), which works anywhere JavaScript runs. + +After choosing your stack, you can either scaffold your app using a [template](./example-apps.mdx), or start from scratch using the documentation for your chosen backend and frontend integration. + +### Adding tRPC to an existing project + +Adding tRPC to an existing project is not significantly different from starting a new project, so the same resources apply. The main challenge is that it can feel difficult to know how to integrate tRPC with your existing application. Here are some tips: + +- You don't need to port all of your existing backend logic to tRPC. A common migration strategy is to initially only use tRPC for new endpoints, and only later migrate existing endpoints to tRPC. +- If you're not sure where to start, check the documentation for your backend [adapter](../server/adapters-intro.md) and frontend implementation, as well as the [example apps](./example-apps.mdx). +- If you are looking for some inspiration of how tRPC might look as part of a larger codebase, there are some examples in [Open-source projects using tRPC](../community/awesome-trpc.mdx#-open-source-projects-using-trpc). + +## Join our Community + +Join us in the [tRPC Discord](https://trpc.io/discord) to share your experiences, ask questions, and get help from the community! diff --git a/.claude/docs/references/trpc/introduction.mdx b/.claude/docs/references/trpc/introduction.mdx new file mode 100644 index 00000000..0e474fec --- /dev/null +++ b/.claude/docs/references/trpc/introduction.mdx @@ -0,0 +1,53 @@ +--- +id: introduction +title: tRPC +hide_title: true +sidebar_label: Introduction +slug: / +author: Alex / KATT 🐱 +author_url: https://twitter.com/alexdotjs +author_image_url: https://avatars1.githubusercontent.com/u/459267?s=460&v=4 +--- + + +## Introduction + +

+ tRPC allows you to + easily build & consume fully typesafe APIs without schemas or code generation. +

+ +As TypeScript and static typing increasingly becomes a best practice in web development, API contracts present a major pain point. We need better ways to **statically type** our API endpoints and **share those types** between our client and server (or server-to-server). We set out to build a simple library for building typesafe APIs that leverages the full power of modern TypeScript. + +### An alternative to traditional REST or GraphQL + +Currently, GraphQL is the dominant way to implement typesafe APIs in TypeScript ([and it's amazing!](../further/further-reading.md#relationship-to-graphql)). Since GraphQL is designed as a language-agnostic specification for implementing APIs, it doesn't take full advantage of the power of a language like TypeScript. + +If your project is built with full-stack TypeScript, you can share types **directly** between your client and server, without relying on code generation. + + + +### Who is tRPC for? + +tRPC is for full-stack TypeScript developers. It makes it easy to write endpoints that you can safely use in both the front and backend of your app. Type errors with your API contracts will be caught at build time, reducing the surface for bugs in your application at runtime. + +## Features + +- ✅  Well-tested and production ready. +- 🧙‍♂️  Full static typesafety & autocompletion on the client, for inputs, outputs, and errors. +- 🐎  Snappy DX - No code generation, run-time bloat, or build pipeline. +- 🍃  Light - tRPC has zero deps and a tiny client-side footprint. +- 🐻  For new and old projects - Easy to start with or add to your existing brownfield project. +- 🔋  Framework agnostic - The tRPC community has built [adapters](https://trpc.io/docs/awesome-trpc#-extensions--community-add-ons) for all of the most popular frameworks. +- 🥃  Subscriptions support - Add typesafe observability to your application. +- ⚡️  Request batching - Requests made at the same time can be automatically combined into one. +- 🤖  AI-friendly - tRPC's widespread adoption means AI coding tools generate more accurate code compared to less common patterns. +- 👀  Examples - Check out an [example](example-apps.mdx) to learn with or use as a starting point. diff --git a/.claude/docs/references/trpc/quickstart.mdx b/.claude/docs/references/trpc/quickstart.mdx new file mode 100644 index 00000000..198e6f9a --- /dev/null +++ b/.claude/docs/references/trpc/quickstart.mdx @@ -0,0 +1,562 @@ +--- +id: quickstart +title: Quickstart +sidebar_label: Quickstart +slug: /quickstart +description: Learn how to quickly get started and setup tRPC +--- + +import CodeBlock from '@theme/CodeBlock'; +import TabItem from '@theme/TabItem'; +import Tabs from '@theme/Tabs'; + +```twoslash include db +type User = { id: string; name: string }; + +// Imaginary database +const users: User[] = []; +export const db = { + user: { + findMany: async () => users, + findById: async (id: string) => users.find((user) => user.id === id), + create: async (data: { name: string }) => { + const user = { id: String(users.length + 1), ...data }; + users.push(user); + return user; + }, + }, +}; +``` + +```twoslash include trpc +import { initTRPC } from '@trpc/server'; + +const t = initTRPC.create(); + +export const router = t.router; +export const publicProcedure = t.procedure; +``` + +```twoslash include server +import { createHTTPServer } from "@trpc/server/adapters/standalone"; +import { z } from "zod"; +import { db } from "./db"; +import { publicProcedure, router } from "./trpc"; + +const appRouter = router({ + userList: publicProcedure + .query(async () => { + const users = await db.user.findMany(); + return users; + }), + userById: publicProcedure + .input(z.string()) + .query(async (opts) => { + const { input } = opts; + const user = await db.user.findById(input); + return user; + }), + userCreate: publicProcedure + .input(z.object({ name: z.string() })) + .mutation(async (opts) => { + const { input } = opts; + const user = await db.user.create(input); + return user; + }), +}); + +export type AppRouter = typeof appRouter; + +const server = createHTTPServer({ + router: appRouter, +}); + +server.listen(3000); +``` + +tRPC combines concepts from [REST](https://www.sitepoint.com/rest-api/) and [GraphQL](https://graphql.org/). If you are unfamiliar with either, take a look at the key [Concepts](./concepts.mdx). + + +## Installation + +tRPC is split between several packages, so you can install only what you need. Make sure to install the packages you want in the proper sections of your codebase. For this quickstart guide we'll keep it simple and use the vanilla client only. For framework guides, checkout [usage with React](/docs/client/tanstack-react-query/setup) and [usage with Next.js](/docs/client/nextjs/setup). + +:::info Requirements +- tRPC requires TypeScript >=5.7.2 +- We strongly recommend you using `"strict": true` in your `tsconfig.json` as we don't officially support non-strict mode. +::: + +Start off by installing the `@trpc/server` and `@trpc/client` packages: + + +import { InstallSnippet } from '@site/src/components/InstallSnippet'; + + + + +## Defining a backend router + +Let's walk through the steps of building a typesafe API with tRPC. To start, this API will contain three endpoints with these TypeScript signatures: + +```ts +type User = { id: string; name: string; }; + +userList: () => User[]; +userById: (id: string) => User; +userCreate: (data: { name: string }) => User; +``` + +### 1. Create a router instance + +First, let's initialize the tRPC backend. It's good convention to do this in a separate file and export reusable helper functions instead of the entire tRPC object. + +```ts twoslash title='server/trpc.ts' +import { initTRPC } from '@trpc/server'; + +/** + * Initialization of tRPC backend + * Should be done only once per backend! + */ +const t = initTRPC.create(); + +/** + * Export reusable router and procedure helpers + * that can be used throughout the router + */ +export const router = t.router; +export const publicProcedure = t.procedure; +``` + +Next, we'll initialize our main router instance, commonly referred to as `appRouter`, in which we'll later add procedures to. Lastly, we need to export the type of the router which we'll later use on the client side. + +```ts twoslash title='server/index.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: server.ts +// ---cut--- +import { router } from './trpc'; + +const appRouter = router({ + // ... +}); + +// Export type router type signature, +// NOT the router itself. +export type AppRouter = typeof appRouter; +``` + +### 2. Add a query procedure + +Use `publicProcedure.query()` to add a query procedure to the router. + +The following creates a query procedure called `userList` that returns a list of users from our database: + +```ts twoslash title='server/index.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: db.ts +// @include: db +// @filename: server.ts +// ---cut--- +import { db } from './db'; +import { publicProcedure, router } from './trpc'; + +const appRouter = router({ + userList: publicProcedure + .query(async () => { + // Retrieve users from a datasource, this is an imaginary database + const users = await db.user.findMany(); + // ^? + return users; + }), +}); +``` + +### 3. Using input parser to validate procedure inputs + +To implement the `userById` procedure, we need to accept input from the client. tRPC lets you define [input parsers](../server/validators.md) to validate and parse the input. You can define your own input parser or use a validation library of your choice, like [zod](https://zod.dev), [yup](https://github.com/jquense/yup), or [superstruct](https://docs.superstructjs.org/). + +You define your input parser on `publicProcedure.input()`, which can then be accessed on the resolver function as shown below: + + + + The input parser should be a function that validates and casts the input of this procedure. It should return a strongly typed value when the input is valid or throw an error if the input is invalid. + +
+
+ +```ts twoslash title='server/index.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: db.ts +// @include: db +// @filename: server.ts +import { db } from './db'; +import { publicProcedure, router } from './trpc'; + +// ---cut--- +const appRouter = router({ + // ... + userById: publicProcedure + // The input is unknown at this time. A client could have sent + // us anything so we won't assume a certain data type. + .input((val: unknown) => { + // If the value is of type string, return it. + // It will now be inferred as a string. + if (typeof val === 'string') return val; + + // Uh oh, looks like that input wasn't a string. + // We will throw an error instead of running the procedure. + throw new Error(`Invalid input: ${typeof val}`); + }) + .query(async (opts) => { + const { input } = opts; + // ^? + // Retrieve the user with the given ID + const user = await db.user.findById(input); + // ^? + return user; + }), +}); +``` + +
+ + The input parser can be any ZodType, e.g. z.string() or z.object({}). + +
+
+ +```ts twoslash title='server.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: db.ts +// @include: db +// @filename: server.ts +import { db } from './db'; +import { publicProcedure, router } from './trpc'; +// ---cut--- +import { z } from 'zod'; + +const appRouter = router({ + // ... + userById: publicProcedure + .input(z.string()) + .query(async (opts) => { + const { input } = opts; + // ^? + // Retrieve the user with the given ID + const user = await db.user.findById(input); + // ^? + return user; + }), +}); +``` + +
+ + The input parser can be any YupSchema, e.g. yup.string() or yup.object({}). + +
+
+ +```ts twoslash title='server.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: db.ts +// @include: db +// @filename: server.ts +import { db } from './db'; +import { publicProcedure, router } from './trpc'; +// ---cut--- +import * as yup from 'yup'; + +const appRouter = router({ + // ... + userById: publicProcedure + .input(yup.string().required()) + .query(async (opts) => { + const { input } = opts; + // ^? + // Retrieve the user with the given ID + const user = await db.user.findById(input); + // ^? + return user; + }), +}); +``` + +
+ + The input parser can be any Valibot schema, e.g. v.string() or v.object({}). + +
+
+ +```ts twoslash title='server.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: db.ts +// @include: db +// @filename: server.ts +import { db } from './db'; +import { publicProcedure, router } from './trpc'; +// ---cut--- +import * as v from 'valibot'; + +const appRouter = router({ + // ... + userById: publicProcedure + .input(v.string()) + .query(async (opts) => { + const { input } = opts; + // ^? + // Retrieve the user with the given ID + const user = await db.user.findById(input); + // ^? + return user; + }), +}); +``` + +
+
+ +:::info +Throughout the remaining of this documentation, we will use `zod` as our validation library. +::: + +### 4. Adding a mutation procedure + +Similar to GraphQL, tRPC makes a distinction between query and mutation procedures. + +The way a procedure works on the server doesn't change much between a query and a mutation. The method name is different, and the way that the client will use this procedure changes - but everything else is the same! + +Let's add a `userCreate` mutation by adding it as a new property on our router object: + +```ts twoslash title='server.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: db.ts +// @include: db +// @filename: server.ts +import { z } from 'zod'; +import { db } from './db'; +import { publicProcedure, router } from './trpc'; +// ---cut--- + +const appRouter = router({ + // ... + userCreate: publicProcedure + .input(z.object({ name: z.string() })) + .mutation(async (opts) => { + const { input } = opts; + // ^? + // Create a new user in the database + const user = await db.user.create(input); + // ^? + return user; + }), +}); +``` + +## Serving the API + +Now that we have defined our router, we can serve it. tRPC has many [adapters](/docs/server/adapters) so you can use any backend framework of your choice. To keep it simple, we'll use the [`standalone`](/docs/server/adapters/standalone) adapter. + +```ts twoslash title='server/index.ts' +// @filename: trpc.ts +// @include: trpc +// @filename: server.ts +import { router } from './trpc'; +// ---cut--- +import { createHTTPServer } from '@trpc/server/adapters/standalone'; + +const appRouter = router({ + // ... +}); + +const server = createHTTPServer({ + router: appRouter, +}); + +server.listen(3000); +``` + +
+See the full backend code + +```ts twoslash title="server/db.ts" +// @include: db +``` + +
+ +```ts twoslash title="server/trpc.ts" +// @include: trpc +``` + +
+ +```ts twoslash title='server/index.ts' +// @filename: db.ts +// @include: db +// @filename: trpc.ts +// @include: trpc +// @filename: server.ts +// ---cut--- +// @include: server +``` + +
+ +## Using your new backend on the client + +Let's now move to the client-side code and embrace the power of end-to-end typesafety. When we import the `AppRouter` type for the client to use, we have achieved full typesafety for our system without leaking any implementation details to the client. + +### 1. Setup the tRPC Client + +```ts twoslash title="client/index.ts" +// @target: esnext +// @filename: db.ts +// @include: db +// @filename: trpc.ts +// @include: trpc +// @filename: server.ts +// @include: server +// @filename: client.ts +// ---cut--- +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import type { AppRouter } from './server'; +// 👆 **type-only** import + +// Pass AppRouter as generic here. 👇 This lets the `trpc` object know +// what procedures are available on the server and their input/output types. +const trpc = createTRPCClient({ + links: [ + httpBatchLink({ + url: 'http://localhost:3000', + }), + ], +}); +``` + +Links in tRPC are similar to links in GraphQL, they let us control the data flow **before** being sent to the server. In the example above, we use the [httpBatchLink](/docs/client/links/httpBatchLink), which automatically batches up multiple calls into a single HTTP request. For more in-depth usage of links, see the [links documentation](/docs/client/links). + +### 2. Querying & mutating + +You now have access to your API procedures on the `trpc` object. Try it out! + +```ts twoslash title="client/index.ts" +// @target: esnext +// @filename: db.ts +// @include: db +// @filename: trpc.ts +// @include: trpc +// @filename: server.ts +// @include: server +// @filename: client.ts +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import type { AppRouter } from './server'; + +const trpc = createTRPCClient({ + links: [ + httpBatchLink({ + url: 'http://localhost:3000', + }), + ], +}); + +// ---cut--- +// Inferred types +const user = await trpc.userById.query('1'); +// ^? + +const createdUser = await trpc.userCreate.mutate({ name: 'sachinraja' }); +// ^? +``` + +### Full autocompletion + +You can open up your Intellisense to explore your API on your frontend. You'll find all of your procedure routes waiting for you along with the methods for calling them. + +```ts twoslash title="client/index.ts" +// @target: esnext +// @filename: db.ts +// @include: db +// @filename: trpc.ts +// @include: trpc +// @filename: server.ts +// @include: server +// @filename: client.ts +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import type { AppRouter } from './server'; + +const trpc = createTRPCClient({ + links: [ + httpBatchLink({ + url: 'http://localhost:3000', + }), + ], +}); + +// ---cut--- +// @errors: 2339 +// Full autocompletion on your routes +trpc.u; +// ^| +``` + +## Try it out for yourself! + +import { Iframe } from '@site/src/components/Iframe'; +import { searchParams } from '@site/src/utils/searchParams'; +import clsx from 'clsx'; + +
+