GuidesTypeScript

Convex with TanStack Start and Better Auth

Create a TanStack Start app on a Convex backend with Better Auth email sign-in through the Convex Better Auth component, using Better Fullstack.

Updated October 9, 2026convextanstack-startbetter-authrealtime

Use this stack when the backend should be Convex: queries, mutations, storage, and auth run in a Convex deployment, and the web app is a TanStack Start project that subscribes to them.

npm create better-fullstack@latest my-convex-app -- \  --ecosystem typescript \  --frontend tanstack-start \  --backend convex \  --runtime none \  --database none \  --orm none \  --auth better-auth \  --api none \  --css-framework tailwind \  --ui-library shadcn-ui

Convex provides the database and the function runtime, so the command sets --runtime, --database, --orm, and --api to none. The CLI rejects a runtime such as --runtime bun with Convex and suggests --runtime none.

What this creates

  • A Convex project in packages/backend/convex with a healthCheck query, a privateData query that checks the session, and an empty schema.
  • Better Auth running inside Convex through the @convex-dev/better-auth component, with email and password sign-in.
  • A TanStack Start app in apps/web with sign-in and sign-up forms, a user menu, and a dashboard that renders different content for signed-in and signed-out users.
  • A TanStack Start server route at /api/auth/$ that forwards auth requests to the Convex deployment.
  • Tailwind CSS and shadcn/ui components in the web app.
  • A Turborepo workspace and a reproducible bts.jsonc stack configuration.

Generated shape

txt
my-convex-app/
  bts.jsonc
  turbo.json
  apps/web/
    .env
    src/
      lib/
        auth-client.ts
        auth-server.ts
      router.tsx
      routes/
        __root.tsx
        index.tsx
        dashboard.tsx
        api/auth/$.ts
  packages/
    backend/
      .env.local
      convex/
        auth.config.ts
        auth.ts
        convex.config.ts
        healthCheck.ts
        http.ts
        privateData.ts
        schema.ts
    env/src/web.ts

convex.config.ts installs the Better Auth component, auth.ts builds the Better Auth instance on the component's adapter, and http.ts registers the auth HTTP routes on the Convex deployment. In the web app, __root.tsx wraps the app in ConvexBetterAuthProvider and reads the auth token on the server through auth-server.ts.

Set up Convex

The app needs a Convex deployment to talk to. Install dependencies and run the setup script, which runs convex dev --configure and asks you to log in and choose a project:

npm installnpm run dev:setup

Better Auth reads BETTER_AUTH_SECRET and SITE_URL from the Convex deployment environment, not from a local file. Set them from packages/backend, as the comments in its generated .env.local describe:

bash
cd packages/backend
npx convex env set BETTER_AUTH_SECRET "$(openssl rand -base64 32)"
npx convex env set SITE_URL http://localhost:3001

Then copy the deployment URLs into apps/web/.env:

VariableValue
VITE_CONVEX_URLThe deployment URL that convex dev writes to packages/backend/.env.local, ending in .convex.cloud.
VITE_CONVEX_SITE_URLThe same deployment's HTTP actions URL, ending in .convex.site.

packages/env/src/web.ts validates both values as URLs. The generated placeholders pass that check, so the app starts, but it cannot reach Convex until both point at your deployment.

Run it locally

npm run dev

Turborepo runs convex dev in packages/backend and vite dev in apps/web. The web app runs on port 3001, which is why SITE_URL uses that origin. The home page shows the healthCheck result, and /dashboard shows the sign-up and sign-in forms until a session exists, then calls privateData.

Add a table and functions

Define tables in packages/backend/convex/schema.ts:

ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  notes: defineTable({
    ownerId: v.string(),
    body: v.string(),
  }).index("by_owner", ["ownerId"]),
});

Then add functions in a new file, such as packages/backend/convex/notes.ts. The Convex Better Auth integration issues tokens Convex can verify, so ctx.auth.getUserIdentity() returns the signed-in user:

ts
import { v } from "convex/values";

import { mutation, query } from "./_generated/server";

export const list = query({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return [];
    return ctx.db
      .query("notes")
      .withIndex("by_owner", (q) => q.eq("ownerId", identity.subject))
      .collect();
  },
});

export const add = mutation({
  args: { body: v.string() },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not signed in");
    await ctx.db.insert("notes", { ownerId: identity.subject, body: args.body });
  },
});

convex dev pushes the change and regenerates convex/_generated. In the web app, read the list with useQuery(api.notes.list) from convex/react, as the generated dashboard does for privateData.

Compatibility notes

  • Better Auth with Convex uses the Convex component, so the auth tables live in the component's own storage rather than in your schema.
  • Payments, email, search, and ecommerce options generate no files with --backend convex; the CLI reports payments as not supported. Keep them none and add those integrations as Convex functions.

Deployment notes

Deploy the Convex functions with npx convex deploy from packages/backend, and set BETTER_AUTH_SECRET and SITE_URL on the production deployment with npx convex env set --prod. SITE_URL must be the public web origin. Build the web app with VITE_CONVEX_URL and VITE_CONVEX_SITE_URL pointing at the production deployment.

Troubleshooting

SymptomCheck
Queries never resolve and sign-in failsReplace the placeholder URLs in apps/web/.env with the deployment's .convex.cloud and .convex.site URLs.
Sign-in fails or redirects to the wrong originSITE_URL in the Convex environment must match the web origin, http://localhost:3001 locally.
api.notes does not exist in the web appKeep npm run dev running so convex dev regenerates convex/_generated.

Comparison notes

Choose Convex when reactive queries and a managed backend matter more than owning a server and database. Compare TanStack Start with PostgreSQL and Drizzle for a self-hosted database, or Hono with Better Auth and Drizzle for a separate API service.

Next steps

GitHub Sponsors