Skip to content

Add auth example #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ const { mutate } = useMutation({
});
```

# Authentication (example TODO)
# 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
Expand Down
6 changes: 6 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import type {
FilterApi,
FunctionReference,
} from "convex/server";
import type * as auth from "../auth.js";
import type * as http from "../http.js";
import type * as messages from "../messages.js";
import type * as user from "../user.js";
import type * as weather from "../weather.js";

/**
Expand All @@ -25,7 +28,10 @@ import type * as weather from "../weather.js";
* ```
*/
declare const fullApi: ApiFromModules<{
auth: typeof auth;
http: typeof http;
messages: typeof messages;
user: typeof user;
weather: typeof weather;
}>;
export declare const api: FilterApi<
Expand Down
8 changes: 8 additions & 0 deletions convex/auth.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: "convex",
},
],
};
6 changes: 6 additions & 0 deletions convex/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Password } from "@convex-dev/auth/providers/Password";
import { convexAuth } from "@convex-dev/auth/server";

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],
});
8 changes: 8 additions & 0 deletions convex/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { httpRouter } from "convex/server";
import { auth } from "./auth";

const http = httpRouter();

auth.addHttpRoutes(http);

export default http;
35 changes: 29 additions & 6 deletions convex/messages.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import { mutation } from "./_generated/server.js";
import { query } from "./_generated/server.js";
import { Doc } from "./_generated/dataModel.js";
import { Doc, Id } from "./_generated/dataModel.js";
import { v } from "convex/values";
import schema, { vv } from "./schema.js";

export const list = query({
handler: async (ctx): Promise<Doc<"messages">[]> => {
return await ctx.db.query("messages").collect();
returns: v.array(
v.object({
...vv.doc("messages").fields,
authorId: v.id("users"),
authorEmail: v.optional(v.string()),
})
),
handler: async (ctx) => {
const messages = await ctx.db.query("messages").collect();
return Promise.all(
messages.map(async (message) => {
const author = await ctx.db.get(message.author);
if (!author) {
throw new Error("Author not found");
}
return { ...message, authorId: author._id, authorEmail: author.email };
})
);
},
});

export const count = query({
handler: async (ctx): Promise<string> => {
returns: v.string(),
handler: async (ctx) => {
const messages = await ctx.db.query("messages").take(1001);
return messages.length === 1001 ? "1000+" : `${messages.length}`;
},
});

export const send = mutation({
handler: async (ctx, { body, author }: { body: string; author: string }) => {
const message = { body, author };
args: {
body: v.string(),
author: v.id("users"),
},
handler: async (ctx, args) => {
const message = { body: args.body, author: args.author };
await ctx.db.insert("messages", message);
},
});
10 changes: 8 additions & 2 deletions convex/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { defineSchema, defineTable } from "convex/server";
import { authTables } from "@convex-dev/auth/server";
import { v } from "convex/values";
import { typedV } from "convex-helpers/validators";

export default defineSchema({
const schema = defineSchema({
...authTables,
messages: defineTable({
author: v.string(),
author: v.id("users"),
body: v.string(),
}),
});
export default schema;

export const vv = typedV(schema);
13 changes: 13 additions & 0 deletions convex/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getAuthUserId } from "@convex-dev/auth/server";
import { Doc } from "./_generated/dataModel";
import { query } from "./_generated/server";

export const getCurrent = query({
handler: async (ctx): Promise<Doc<"users"> | null> => {
const userId = await getAuthUserId(ctx);
if (!userId) {
throw new Error("Unauthorized");
}
return await ctx.db.get(userId);
},
});
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"convex": "^1.24.1"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.74.7",
"@tanstack/react-query": "^5.62.0",
"@tanstack/eslint-plugin-query": "^5.59.7",
"@tanstack/react-query-devtools": "^5.62.0",
"@types/node": "^18.17.0",
"@types/react": "^18.0.0",
Expand Down Expand Up @@ -71,5 +71,13 @@
},
"module": "./dist/esm/index.js",
"main": "./dist/commonjs/index.js",
"types": "./dist/commonjs/index.d.ts"
"types": "./dist/commonjs/index.d.ts",
"dependencies": {
"@auth/core": "^0.37.0",
"@convex-dev/auth": "^0.0.83",
"convex-helpers": "^0.1.85"
},
"overrides": {
"typescript": "~5.0.3"
}
}
91 changes: 80 additions & 11 deletions src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
useSuspenseQuery,
} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import {
Authenticated,
AuthLoading,
ConvexProvider,
ConvexReactClient,
Unauthenticated,
} from "convex/react";
import ReactDOM from "react-dom/client";
import {
ConvexQueryClient,
Expand All @@ -18,6 +24,7 @@ import {
import "./index.css";
import { FormEvent, useState } from "react";
import { api } from "../convex/_generated/api.js";
import { ConvexAuthProvider, useAuthActions } from "@convex-dev/auth/react";

// Build a global convexClient wherever you would normally create a TanStack Query client.
const convexClient = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
Expand All @@ -38,19 +45,70 @@ convexQueryClient.connect(queryClient);

function Main() {
return (
<ConvexProvider client={convexClient}>
<ConvexAuthProvider client={convexClient}>
<QueryClientProvider client={queryClient}>
<App />
<AuthLoading>
<div>Loading...</div>
</AuthLoading>
<Unauthenticated>
<SignIn />
</Unauthenticated>
<Authenticated>
<App />
</Authenticated>
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
</ConvexProvider>
</ConvexAuthProvider>
);
}

function SignIn() {
const { signIn } = useAuthActions();
const [step, setStep] = useState<"signUp" | "signIn">("signIn");
return (
<div className="signin-container">
<form
className="signin-form"
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
void signIn("password", formData);
}}
>
<input
name="email"
placeholder="Email"
type="text"
className="signin-input"
/>
<input
name="password"
placeholder="Password"
type="password"
className="signin-input"
/>
<input name="flow" type="hidden" value={step} />
<button type="submit">
{step === "signIn" ? "Sign in" : "Sign up"}
</button>
<button
type="button"
className="signin-secondary"
onClick={() => {
setStep(step === "signIn" ? "signUp" : "signIn");
}}
>
{step === "signIn" ? "Sign up instead" : "Sign in instead"}
</button>
</form>
</div>
);
}

function Weather() {
const { data, isPending, error } = useQuery(
// This query doesn't update reactively, it refetches like a normal queryFn.
convexAction(api.weather.getSFWeather, {}),
convexAction(api.weather.getSFWeather, {})
);
if (isPending || error) return <span>?</span>;
const fetchedAt = new Date(data.fetchedAt);
Expand All @@ -71,7 +129,7 @@ function MessageCount() {
const [shown, setShown] = useState(true);
// This is a conditional query
const { data, isPending, error } = useQuery(
convexQuery(api.messages.count, shown ? {} : "skip"),
convexQuery(api.messages.count, shown ? {} : "skip")
);
return (
<div className="message-count">
Expand All @@ -90,25 +148,33 @@ function MessageCount() {
}

function App() {
const { signOut } = useAuthActions();
const { data, error, isPending } = useQuery({
// This query updates reactively.
...convexQuery(api.messages.list, {}),
initialData: [],
});
const {
data: user,
error: userError,
isPending: userIsPending,
} = useQuery({
...convexQuery(api.user.getCurrent, {}),
initialData: null,
});

const [newMessageText, setNewMessageText] = useState("");
const { mutate, isPending: sending } = useMutation({
mutationFn: useConvexMutation(api.messages.send),
});
const [name] = useState(() => "User " + Math.floor(Math.random() * 10000));
async function handleSendMessage(event: FormEvent) {
event.preventDefault();
if (!sending && newMessageText) {
mutate(
{ body: newMessageText, author: name },
{ body: newMessageText, author: user?._id },
{
onSuccess: () => setNewMessageText(""),
},
}
);
}
}
Expand All @@ -120,16 +186,19 @@ function App() {
}
return (
<main>
<button type="button" onClick={() => void signOut()}>
Sign out
</button>
<h1>Convex Chat</h1>
<Weather />
<MessageCount />
<p className="badge">
<span>{name}</span>
<span>{user?.email}</span>
</p>
<ul>
{data.map((message) => (
<li key={message._id}>
<span>{message.author}:</span>
<span>{message.authorEmail}:</span>
<span>{message.body}</span>
<span>{new Date(message._creationTime).toLocaleTimeString()}</span>
</li>
Expand Down
38 changes: 36 additions & 2 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
}

body {
font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", helvetica,
sans-serif;
font-family:
system-ui, "Segoe UI", Roboto, "Helvetica Neue", helvetica, sans-serif;
}

main {
Expand Down Expand Up @@ -123,3 +123,37 @@ input[type="submit"]:disabled,
button:disabled {
background-color: rgb(122, 160, 248);
}

.signin-container {
display: flex;
justify-content: center;
margin-top: 32px;
}

.signin-form {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}

.signin-input {
border: 1px solid #ced4da;
border-radius: 8px;
padding: 6px 12px;
font-size: 16px;
color: rgb(33, 37, 41);
background: white;
}

.signin-secondary {
background: none;
color: #316cf4;
box-shadow: none;
border: none;
padding: 0;
font-size: 16px;
margin-left: 0;
margin-top: 4px;
cursor: pointer;
}