convex-monorepo Template

repository·main·Indexed 18 days ago

https://github.com/get-convex/turbo-expo-nextjs-clerk-convex-monorepo

A TypeScript monorepo template for building shared note-taking applications across Web (Next.js) and Native (Expo) platforms. It features a backend powered by Convex, authentication via Clerk, and uses Turborepo for orchestration. The template includes guides for implementing Convex query and mutation functions, configuring environment variables for Clerk and OpenAI, and deploying the backend and web application.

Tokens
6.8K
Snippets
33
Records
35
Agent score
64%

What's inside convex-monorepo

  1. Deploy the backend and web app

    main

    To deploy the backend and build the web app simultaneously, run the following command from the root. This command uses Convex to deploy the backend and then triggers the web app build, passing the NEXT_PUBLIC_CONVEX_URL environment variable.

    Note: apps/web/vercel.json is pre-configured for this workflow on Vercel.

    cd ../../packages/backend && pnpm exec convex deploy --cmd 'cd ../../apps/web && pnpm build' --cmd-url-env-var-name NEXT_PUBLIC_CONVEX_URL
  2. Configure app-specific environment variables

    main

    You must create .env.local files in both apps/web and apps/native by copying the provided .example.env files. Ensure the following mappings are correct:

    • Convex URL: Copy the CONVEX_URL from packages/backend/.env.local and use it for NEXT_PUBLIC_CONVEX_URL (in apps/web) and EXPO_PUBLIC_CONVEX_URL (in apps/native).
    • Clerk Keys:
      • Use your Clerk publishable key in both apps/web/.env.local and apps/native/.env.local.
      • Use your Clerk secret key in apps/web/.env.local only.
  3. Add dependencies to specific packages

    main

    Use the --filter flag with pnpm to add dependencies to a specific package within the monorepo.

    Examples:

    • For the web app: pnpm --filter web-app add <package>@latest
    • For the native app: pnpm --filter native-app add <package>@latest
    • For the backend: pnpm --filter @packages/backend add <package>@latest
    pnpm --filter web-app add mypackage@latest
    pnpm --filter native-app add mypackage@latest
    pnpm --filter @packages/backend add mypackage@latest
  4. Configure environment variables for Clerk and OpenAI

    main

    To enable authentication and AI features, add the following to your Convex environment variables:

    • Clerk Authentication: CLERK_JWT_ISSUER_DOMAIN=https://your-frontend-api.clerk.accounts.dev
    • AI Summaries: OPENAI_API_KEY=...
    CLERK_JWT_ISSUER_DOMAIN=https://your-frontend-api.clerk.accounts.dev
    OPENAI_API_KEY=...
  5. Quick start guide for the Fullstack Monorepo Template

    main

    Follow these steps to set up the development environment for the web and native apps:

    1. Install dependencies: Run pnpm install at the root.
    2. Configure Convex: Run pnpm --filter @packages/backend setup to log in, create/connect a project, and generate packages/backend/.env.local.
    3. Configure Clerk for Convex: Follow the Convex + Clerk guide. Add your Clerk JWT issuer domain to Convex environment variables as CLERK_JWT_ISSUER_DOMAIN.
    4. Configure app env files: Create .env.local files in apps/web and apps/native using the .example.env files as templates.
    5. Run the apps: Run pnpm dev to start the backend, web app, and native app simultaneously via Turborepo.
    pnpm install
    pnpm --filter @packages/backend setup
    pnpm dev
  6. Define internal functions with internalQuery, internalMutation, and internalAction

    main

    If you need to define functions that can read from or modify the database but should not be accessible from the client, use the internal variants. These are only accessible from other Convex functions.

    • internalQuery: Accessible only from other Convex functions; can read the database.
    • internalMutation: Accessible only from other Convex functions; can modify the database.
    • internalAction: Accessible only from other Convex functions; can execute side-effects.
    import { internalQuery, internalMutation, internalAction } from "./_generated/server";
    
    export const myInternalQuery = internalQuery({ ... });
    export const myInternalMutation = internalMutation({ ... });
    export const myInternalAction = internalAction({ ... });
  7. Configure Clerk middleware for route protection in Next.js

    main

    The middleware.ts (or proxy.ts in this context) uses Clerk's clerkMiddleware to intercept requests and enforce authentication. You can define protected routes using createRouteMatcher with an array of path patterns. If a request matches a protected route, auth.protect() is called to ensure the user is authenticated; otherwise, the request proceeds normally.

    To ensure the middleware runs on the correct routes, export a config object with a matcher array. The provided pattern is a standard Next.js middleware matcher that excludes static assets and internal Next.js files while including API and TRPC routes.

    import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
    
    // Define which routes require authentication
    const isProtectedRoute = createRouteMatcher(["/notes(.*)"]);
    
    export default clerkMiddleware(async (auth, request) => {
      // If the route is not protected, do nothing
      if (!isProtectedRoute(request)) return;
    
      // Enforce authentication for protected routes
      await auth.protect();
    });
    
    // Configure the middleware matcher
    export const config = {
      matcher: [
        "/((?!_next|[^?]*\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
        "/(api|trpc)(.*)",
      ],
    };
  8. Use a Convex mutation function in a React component

    main

    To trigger a mutation in a React component, use the useMutation hook. This returns a function that you can call to execute the mutation. You can either 'fire and forget' or await the result using .then() or await.

    const mutation = useMutation(api.functions.myMutationFunction);
    
    function handleButtonPress() {
      // fire and forget, the most common way to use mutations
      mutation({ first: "Hello!", second: "me" });
    
      // OR
      // use the result once the mutation has completed
      mutation({ first: "Hello!", second: "me" }).then((result) =>
        console.log(result),
      );
    }
  9. Write a Convex mutation function

    main

    A mutation function is used to modify data in the database (insert, update, or delete). Like queries, mutations use the mutation function from ./_generated/server, require an args schema using v from convex/values, and an async handler. Mutations can also read from the database using ctx.db.

    // functions.js
    import { mutation } from "./_generated/server";
    import { v } from "convex/values";
    
    export const myMutationFunction = mutation({
      // Validators for arguments.
      args: {
        first: v.string(),
        second: v.string(),
      },
    
      // Function implementation.
      handler: async (ctx, args) => {
        // Insert or modify documents in the database here.
        const message = { body: args.first, author: args.second };
        const id = await ctx.db.insert("messages", message);
    
        // Optionally, return a value from your mutation.
        return await ctx.db.get(id);
      },
    });
  10. Configure Clerk authentication for Convex

    main

    To enable Clerk authentication within your Convex backend, you must define an auth.config.ts file that specifies the Clerk JWT issuer domain. This configuration allows Convex to validate JWTs issued by Clerk.

    1. Ensure the environment variable CLERK_JWT_ISSUER_DOMAIN is set in your Convex deployment environment.
    2. Export a default object satisfying the AuthConfig type containing a providers array.
    3. The provider must include the domain (matching your Clerk issuer domain) and the applicationID set to "convex".
    import { type AuthConfig } from "convex/server";
    
    const clerkJwtIssuerDomain = process.env.CLERK_JWT_ISSUER_DOMAIN;
    
    if (!clerkJwtIssuerDomain)
      throw new Error(
        "Missing CLERK_JWT_ISSUER_DOMAIN in Convex environment variables",
      );
    
    export default {
      providers: [
        {
          domain: clerkJwtIssuerDomain,
          applicationID: "convex",
        },
      ],
    } satisfies AuthConfig;