├── src ├── blabla.ts ├── vite-env.d.ts ├── LogoutButton.tsx ├── App.tsx ├── App.css ├── main.tsx ├── SessionProvider.tsx ├── usingSession.tsx ├── LoginForm.tsx ├── index.css └── assets │ └── react.svg ├── screenshot.png ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── convex ├── env.d.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── crons.ts ├── tsconfig.json ├── schema.ts ├── users.ts ├── README.md ├── withAuth.ts └── lucia.ts ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── package.json ├── public └── vite.svg ├── README.md └── LICENSE /src/blabla.ts: -------------------------------------------------------------------------------- 1 | export type Foo = "string"; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-lucia-auth-demo/main/screenshot.png -------------------------------------------------------------------------------- /src/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { useSetSessionId } from "./SessionProvider"; 3 | 4 | export function LogoutButton() { 5 | const setSessionId = useSetSessionId(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | outDir: "docs", 9 | }, 10 | base: "/convex-lucia-auth-demo/", 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env.production 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /convex/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Lucia { 2 | type Auth = import("./lucia").Auth; 3 | type DatabaseUserAttributes = { 4 | _id: import("./_generated/dataModel").Id<"users">; 5 | _creationTime: number; 6 | email: string; 7 | }; 8 | type DatabaseSessionAttributes = { 9 | _id: import("./_generated/dataModel").Id<"sessions">; 10 | _creationTime: number; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Convex + Lucia Auth 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /convex/crons.ts: -------------------------------------------------------------------------------- 1 | import { cronJobs } from "convex/server"; 2 | import { internal } from "./_generated/api"; 3 | import { internalMutation } from "./_generated/server"; 4 | import { getAuth } from "./lucia"; 5 | 6 | const crons = cronJobs(); 7 | 8 | crons.daily( 9 | "clear stale sessions and keys", 10 | { hourUTC: 8, minuteUTC: 0 }, 11 | internal.crons.clearStaleSessionsAndKeys 12 | ); 13 | 14 | export const clearStaleSessionsAndKeys = internalMutation(async (ctx) => { 15 | const sessions = await ctx.db.query("sessions").collect(); 16 | for (const session of sessions) { 17 | await getAuth(ctx.db).deleteDeadUserSessions(session.user_id); 18 | } 19 | }); 20 | 21 | export default crons; 22 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "../convex/_generated/api"; 2 | import "./App.css"; 3 | import { LoginForm } from "./LoginForm"; 4 | import { LogoutButton } from "./LogoutButton"; 5 | import { useQuery } from "./usingSession"; 6 | 7 | function App() { 8 | const user = useQuery(api.users.get, {}); 9 | 10 | return ( 11 |
12 |

Convex + Lucia Auth Example

13 | {user === undefined ? ( 14 |
Loading...
15 | ) : user == null ? ( 16 | 17 | ) : ( 18 |
19 |

Welcome {user.email}

20 | 21 |
22 | )} 23 |
24 | ); 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": [ 14 | "warn", 15 | { allowConstantExport: true }, 16 | ], 17 | "@typescript-eslint/no-explicit-any": "off", 18 | "@typescript-eslint/no-unused-vars": [ 19 | "error", 20 | { 21 | argsIgnorePattern: "^_", 22 | varsIgnorePattern: "^_", 23 | }, 24 | ], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src", "convex"], 24 | "exclude": ["convex/_generated"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"], 24 | } 25 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | text-align: center; 8 | } 9 | 10 | main { 11 | flex-grow: 1; 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | } 16 | 17 | .logo { 18 | height: 6em; 19 | padding: 1.5em; 20 | will-change: filter; 21 | transition: filter 300ms; 22 | } 23 | .logo:hover { 24 | filter: drop-shadow(0 0 2em #646cffaa); 25 | } 26 | .logo.react:hover { 27 | filter: drop-shadow(0 0 2em #61dafbaa); 28 | } 29 | 30 | @keyframes logo-spin { 31 | from { 32 | transform: rotate(0deg); 33 | } 34 | to { 35 | transform: rotate(360deg); 36 | } 37 | } 38 | 39 | @media (prefers-reduced-motion: no-preference) { 40 | a:nth-of-type(2) .logo { 41 | animation: logo-spin infinite 20s linear; 42 | } 43 | } 44 | 45 | .card { 46 | padding: 2em; 47 | } 48 | 49 | .read-the-docs { 50 | color: #888; 51 | } 52 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import App from "./App"; 5 | import { SessionProvider } from "./SessionProvider"; 6 | import "./index.css"; 7 | 8 | const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); 9 | 10 | ReactDOM.createRoot(document.getElementById("root")!).render( 11 | 12 | 13 | 14 | 15 |
16 | Built on{" "} 17 | 18 | Convex 19 | 20 | , source on{" "} 21 | 25 | Github 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as crons from "../crons"; 18 | import type * as lucia from "../lucia"; 19 | import type * as users from "../users"; 20 | import type * as withAuth from "../withAuth"; 21 | 22 | /** 23 | * A utility for referencing Convex functions in your app's API. 24 | * 25 | * Usage: 26 | * ```js 27 | * const myFunctionReference = api.myModule.myFunction; 28 | * ``` 29 | */ 30 | declare const fullApi: ApiFromModules<{ 31 | crons: typeof crons; 32 | lucia: typeof lucia; 33 | users: typeof users; 34 | withAuth: typeof withAuth; 35 | }>; 36 | export declare const api: FilterApi< 37 | typeof fullApi, 38 | FunctionReference 39 | >; 40 | export declare const internal: FilterApi< 41 | typeof fullApi, 42 | FunctionReference 43 | >; 44 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { Validator, v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | ...authTables({ 6 | user: { email: v.string() }, 7 | session: {}, 8 | }), 9 | }); 10 | 11 | function authTables< 12 | UserFields extends Record>, 13 | SchemaFields extends Record> 14 | >({ user, session }: { user: UserFields; session: SchemaFields }) { 15 | return { 16 | users: defineTable({ 17 | ...user, 18 | id: v.string(), 19 | }).index("byId", ["id"]), 20 | sessions: defineTable({ 21 | ...session, 22 | id: v.string(), 23 | user_id: v.string(), 24 | active_expires: v.float64(), 25 | idle_expires: v.float64(), 26 | }) 27 | // `as any` because TypeScript can't infer the table fields correctly 28 | .index("byId", ["id" as any]) 29 | .index("byUserId", ["user_id" as any]), 30 | auth_keys: defineTable({ 31 | id: v.string(), 32 | hashed_password: v.union(v.string(), v.null()), 33 | user_id: v.string(), 34 | }) 35 | .index("byId", ["id"]) 36 | .index("byUserId", ["user_id"]), 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lucia-full-test", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "npm-run-all dev:init --parallel dev:frontend dev:backend", 8 | "dev:backend": "convex dev", 9 | "dev:frontend": "vite --open --clearScreen false", 10 | "dev:init": "convex dev --until-success", 11 | "build": "tsc && vite build", 12 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 13 | "preview": "vite preview" 14 | }, 15 | "dependencies": { 16 | "@types/node": "^20.4.6", 17 | "convex": "^1.0.2", 18 | "crypto-js": "^4.1.1", 19 | "lucia": "^2.0.0", 20 | "npm-run-all": "^4.1.5", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0" 23 | }, 24 | "devDependencies": { 25 | "@types/crypto-js": "^4.1.1", 26 | "@types/react": "^18.2.15", 27 | "@types/react-dom": "^18.2.7", 28 | "@typescript-eslint/eslint-plugin": "^6.0.0", 29 | "@typescript-eslint/parser": "^6.0.0", 30 | "@vitejs/plugin-react": "^4.0.3", 31 | "eslint": "^8.45.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.4.3", 34 | "typescript": "^5.0.2", 35 | "vite": "^4.4.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SessionProvider.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | 3 | import { createContext, useCallback, useContext, useState } from "react"; 4 | 5 | const SessionContext = createContext(undefined as any); 6 | 7 | export function useSessionId(): string | null { 8 | return useContext(SessionContext).sessionId; 9 | } 10 | 11 | export function useSetSessionId(): (sessionId: string | null) => void { 12 | return useContext(SessionContext).setSessionId; 13 | } 14 | 15 | export function SessionProvider({ children }: { children: React.ReactNode }) { 16 | const [sessionId, setSessionIdState] = useState(getSavedSessionId()); 17 | const setSessionId = useCallback( 18 | (value: string | null) => { 19 | setSavedSessionId(value); 20 | setSessionIdState(value); 21 | }, 22 | [setSessionIdState] 23 | ); 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | } 30 | 31 | function getSavedSessionId() { 32 | return localStorage.getItem("sessionId"); 33 | } 34 | 35 | export function setSavedSessionId(sessionId: string | null) { 36 | if (sessionId == null) { 37 | localStorage.removeItem("sessionId"); 38 | } else { 39 | localStorage.setItem("sessionId", sessionId); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/usingSession.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useQuery as useConvexQuery, 3 | useMutation as useConvexMutation, 4 | } from "convex/react"; 5 | import { ReactMutation } from "convex/react-internal"; 6 | import { FunctionReference } from "convex/server"; 7 | import { useCallback } from "react"; 8 | import { useSessionId } from "./SessionProvider"; 9 | 10 | export function useQuery< 11 | Args extends { sessionId: string | null }, 12 | Query extends FunctionReference<"query", "public", Args> 13 | >( 14 | query: Query, 15 | args: Omit 16 | ): Query["_returnType"] | undefined { 17 | const sessionId = useSessionId(); 18 | return useConvexQuery(query, { ...args, sessionId } as any); 19 | } 20 | 21 | export function useMutation< 22 | Args extends { sessionId: string | null }, 23 | Mutation extends FunctionReference<"mutation", "public", Args> 24 | >( 25 | mutation: Mutation 26 | ): ReactMutation< 27 | FunctionReference<"mutation", "public", Omit> 28 | > { 29 | const doMutation = useConvexMutation(mutation); 30 | const sessionId = useSessionId(); 31 | return useCallback( 32 | (args: Omit) => { 33 | return doMutation({ ...args, sessionId } as any); 34 | }, 35 | [doMutation, sessionId] 36 | ) as any; // We don't support optimistic updates 37 | } 38 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /convex/users.ts: -------------------------------------------------------------------------------- 1 | import { v } from "convex/values"; 2 | import { queryWithAuth, mutationWithAuth } from "./withAuth"; 3 | import { Id } from "./_generated/dataModel"; 4 | 5 | export const get = queryWithAuth({ 6 | args: {}, 7 | handler: async (ctx) => { 8 | return ctx.session?.user; 9 | }, 10 | }); 11 | 12 | export const signIn = mutationWithAuth({ 13 | args: { 14 | email: v.string(), 15 | password: v.string(), 16 | }, 17 | handler: async (ctx, { email, password }) => { 18 | const key = await ctx.auth.useKey("password", email, password); 19 | const session = await ctx.auth.createSession({ 20 | userId: key.userId, 21 | attributes: { 22 | // These will be filled out by Convex 23 | _id: "" as Id<"sessions">, 24 | _creationTime: 0, 25 | }, 26 | }); 27 | return session.sessionId; 28 | }, 29 | }); 30 | 31 | export const signUp = mutationWithAuth({ 32 | args: { 33 | email: v.string(), 34 | password: v.string(), 35 | }, 36 | handler: async (ctx, { email, password }) => { 37 | const user = await ctx.auth.createUser({ 38 | key: { 39 | password: password, 40 | providerId: "password", 41 | providerUserId: email, 42 | }, 43 | attributes: { 44 | email, 45 | // These will be filled out by Convex 46 | _id: "" as Id<"users">, 47 | _creationTime: 0, 48 | }, 49 | }); 50 | const session = await ctx.auth.createSession({ 51 | userId: user.userId, 52 | attributes: { 53 | // These will be filled out by Convex 54 | _id: "" as Id<"sessions">, 55 | _creationTime: 0, 56 | }, 57 | }); 58 | return session.sessionId; 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /src/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useSetSessionId } from "./SessionProvider"; 3 | import { useMutation } from "./usingSession"; 4 | import { api } from "../convex/_generated/api"; 5 | 6 | export function LoginForm() { 7 | const setSessionId = useSetSessionId(); 8 | const [flow, setFlow] = useState<"signIn" | "signUp">("signIn"); 9 | const signIn = useMutation(api.users.signIn); 10 | const signUp = useMutation(api.users.signUp); 11 | 12 | const handleSubmit = async (event: React.FormEvent) => { 13 | event.preventDefault(); 14 | const data = new FormData(event.currentTarget); 15 | try { 16 | const sessionId = await (flow === "signIn" ? signIn : signUp)({ 17 | email: (data.get("email") as string | null) ?? "", 18 | password: (data.get("password") as string | null) ?? "", 19 | }); 20 | setSessionId(sessionId); 21 | } catch { 22 | alert( 23 | flow === "signIn" ? "Invalid email or password" : "Email already in use" 24 | ); 25 | } 26 | }; 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | {flow === "signIn" ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | { 41 | setFlow(flow === "signIn" ? "signUp" : "signIn"); 42 | }} 43 | > 44 | {flow === "signIn" ? ( 45 | <>Don't have an account? Sign up 46 | ) : ( 47 | <>Already have an account? Sign in 48 | )} 49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { DataModelFromSchemaDefinition } from "convex/server"; 13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server"; 14 | import type { GenericId } from "convex/values"; 15 | import schema from "../schema"; 16 | 17 | /** 18 | * The names of all of your Convex tables. 19 | */ 20 | export type TableNames = TableNamesInDataModel; 21 | 22 | /** 23 | * The type of a document stored in Convex. 24 | * 25 | * @typeParam TableName - A string literal type of the table name (like "users"). 26 | */ 27 | export type Doc = DocumentByName< 28 | DataModel, 29 | TableName 30 | >; 31 | 32 | /** 33 | * An identifier for a document in Convex. 34 | * 35 | * Convex documents are uniquely identified by their `Id`, which is accessible 36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 37 | * 38 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 39 | * 40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 41 | * strings when type checking. 42 | * 43 | * @typeParam TableName - A string literal type of the table name (like "users"). 44 | */ 45 | export type Id = GenericId; 46 | 47 | /** 48 | * A type describing your Convex data model. 49 | * 50 | * This type includes information about what tables you have, the type of 51 | * documents stored in those tables, and the indexes defined on them. 52 | * 53 | * This type is used to parameterize methods like `queryGeneric` and 54 | * `mutationGeneric` to make them type-safe. 55 | */ 56 | export type DataModel = DataModelFromSchemaDefinition; 57 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | input[type="submit"], 39 | button { 40 | font-size: 1em; 41 | font-weight: 500; 42 | font-family: inherit; 43 | background-color: #1a1a1a; 44 | cursor: pointer; 45 | transition: border-color 0.25s; 46 | } 47 | 48 | input[type="submit"], 49 | button, 50 | input { 51 | padding: 0.8em 1.2em; 52 | font-size: 1em; 53 | border-radius: 4px; 54 | border: 1px solid rgb(82, 82, 82); 55 | } 56 | 57 | input[type="submit"]:hover, 58 | button:hover { 59 | border-color: #646cff; 60 | } 61 | input[type="submit"]:focus, 62 | input[type="submit"]:focus-visible, 63 | button:focus, 64 | button:focus-visible { 65 | outline: 4px auto -webkit-focus-ring-color; 66 | } 67 | 68 | form { 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: center; 72 | flex-grow: 1; 73 | gap: 0.2em; 74 | width: 19em; 75 | } 76 | 77 | label { 78 | text-align: left; 79 | } 80 | 81 | input { 82 | margin-bottom: 1em; 83 | } 84 | 85 | @media (prefers-color-scheme: light) { 86 | :root { 87 | color: #213547; 88 | background-color: #ffffff; 89 | } 90 | a:hover { 91 | color: #747bff; 92 | } 93 | input[type="submit"], 94 | button { 95 | background-color: #f9f9f9; 96 | } 97 | input[type="submit"], 98 | button, 99 | input { 100 | border-color: rgb(195, 195, 195); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /convex/withAuth.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, PropertyValidators, v } from "convex/values"; 2 | import { Session } from "lucia"; 3 | import { 4 | DatabaseWriter, 5 | MutationCtx, 6 | QueryCtx, 7 | internalMutation, 8 | internalQuery, 9 | mutation, 10 | query, 11 | } from "./_generated/server"; 12 | import { Auth, getAuth } from "./lucia"; 13 | 14 | export function queryWithAuth< 15 | ArgsValidator extends PropertyValidators, 16 | Output 17 | >({ 18 | args, 19 | handler, 20 | }: { 21 | args: ArgsValidator; 22 | handler: ( 23 | ctx: Omit & { session: Session | null }, 24 | args: ObjectType 25 | ) => Output; 26 | }) { 27 | return query({ 28 | args: { 29 | ...args, 30 | sessionId: v.union(v.null(), v.string()), 31 | }, 32 | handler: async (ctx, args: any) => { 33 | const session = await getValidExistingSession(ctx, args.sessionId); 34 | return handler({ ...ctx, session }, args); 35 | }, 36 | }); 37 | } 38 | 39 | export function internalQueryWithAuth< 40 | ArgsValidator extends PropertyValidators, 41 | Output 42 | >({ 43 | args, 44 | handler, 45 | }: { 46 | args: ArgsValidator; 47 | handler: ( 48 | ctx: Omit & { session: Session | null }, 49 | args: ObjectType 50 | ) => Output; 51 | }) { 52 | return internalQuery({ 53 | args: { ...args, sessionId: v.union(v.null(), v.string()) }, 54 | handler: async (ctx, args: any) => { 55 | const session = await getValidExistingSession(ctx, args.sessionId); 56 | return handler({ ...ctx, session }, args); 57 | }, 58 | }); 59 | } 60 | 61 | export function mutationWithAuth< 62 | ArgsValidator extends PropertyValidators, 63 | Output 64 | >({ 65 | args, 66 | handler, 67 | }: { 68 | args: ArgsValidator; 69 | handler: ( 70 | ctx: Omit & { auth: Auth; session: Session | null }, 71 | args: ObjectType 72 | ) => Output; 73 | }) { 74 | return mutation({ 75 | args: { ...args, sessionId: v.union(v.null(), v.string()) }, 76 | handler: async (ctx, args: any) => { 77 | const auth = getAuth(ctx.db); 78 | const session = await getValidSessionAndRenew(auth, args.sessionId); 79 | return handler({ ...ctx, session, auth }, args); 80 | }, 81 | }); 82 | } 83 | 84 | export function internalMutationWithAuth< 85 | ArgsValidator extends PropertyValidators, 86 | Output 87 | >({ 88 | args, 89 | handler, 90 | }: { 91 | args: ArgsValidator; 92 | handler: ( 93 | ctx: Omit & { auth: Auth; session: Session | null }, 94 | args: ObjectType 95 | ) => Output; 96 | }) { 97 | return internalMutation({ 98 | args: { ...args, sessionId: v.union(v.null(), v.string()) }, 99 | handler: async (ctx, args: any) => { 100 | const auth = getAuth(ctx.db); 101 | const session = await getValidSessionAndRenew(auth, args.sessionId); 102 | return handler({ ...ctx, session, auth }, args); 103 | }, 104 | }); 105 | } 106 | 107 | async function getValidExistingSession( 108 | ctx: QueryCtx, 109 | sessionId: string | null 110 | ) { 111 | if (sessionId === null) { 112 | return null; 113 | } 114 | // The cast is OK because we will only expose the existing session 115 | const auth = getAuth(ctx.db as DatabaseWriter); 116 | try { 117 | const session = (await auth.getSession(sessionId)) as Session | null; 118 | if (session === null || session.state === "idle") { 119 | return null; 120 | } 121 | return session; 122 | } catch (error) { 123 | // Invalid session ID 124 | return null; 125 | } 126 | } 127 | 128 | async function getValidSessionAndRenew(auth: Auth, sessionId: string | null) { 129 | if (sessionId === null) { 130 | return null; 131 | } 132 | try { 133 | return await auth.validateSession(sessionId); 134 | } catch (error) { 135 | // Invalid session ID 136 | return null; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convex Lucia Auth Demo 2 | 3 | This example app showcases authentication built entirely on top of Convex without any third-party platform. It uses [Lucia](https://lucia-auth.com/) for the authentication logic. 4 | 5 | ![Screenshot of the app](./screenshot.png) 6 | 7 | Features: 8 | 9 | - Without any additional setup, you can sign in with an email+password combination 10 | - Sign out button 11 | - Session is preserved in `localStorage` 12 | - Passwords are securely hashed 13 | 14 | This integration works! You can see a production deployment at this live site: https://get-convex.github.io/convex-lucia-auth-demo/. 15 | 16 | Read a deep dive into this codebase at [Custom Authentication (with Lucia)](https://stack.convex.dev/convex-with-lucia) on Stack. 17 | 18 | ## Setting up 19 | 20 | Run: 21 | 22 | ``` 23 | npm install 24 | npm run dev 25 | ``` 26 | 27 | This will guide you through making a Convex project, and then open the web browser with the app running. 28 | 29 | ## Deploying to production 30 | 31 | Follow these steps to deploy this repo to production: 32 | 33 | 1. Configure `VITE_CONVEX_URL` either in your hosting provider or in a `.env.production` file 34 | 2. In your production deployment's settings page configure these variables: 35 | 36 | - `LUCIA_ENVIRONMENT`=`PROD` 37 | - `HOSTNAME`=where the app is hosted, such as `foo.github.io` or `mydomain.com` 38 | 39 | ## Note on CSRF protection 40 | 41 | This demo uses `localStorage` for storing the secret `sessionId`. This means that sessions are only preserved on pages served on the same subdomain, such as `foo.example.com` or `username.github.io`. This prevents CSRF attacks. 42 | 43 | This does though invite an [XSS attack](https://en.wikipedia.org/wiki/Cross-site_scripting). Make sure your app is not susceptable to XSS. 44 | 45 | Convex currently doesn't support accessing cookies in queries and mutations, so cookie-based authentication can only be used in Convex HTTP actions (not demonstrated in this demo). 46 | 47 | # What is Convex? 48 | 49 | [Convex](https://convex.dev) is a hosted backend platform with a 50 | built-in database that lets you write your 51 | [database schema](https://docs.convex.dev/database/schemas) and 52 | [server functions](https://docs.convex.dev/functions) in 53 | [TypeScript](https://docs.convex.dev/typescript). Server-side database 54 | [queries](https://docs.convex.dev/functions/query-functions) automatically 55 | [cache](https://docs.convex.dev/functions/query-functions#caching--reactivity) and 56 | [subscribe](https://docs.convex.dev/client/react#reactivity) to data, powering a 57 | [realtime `useQuery` hook](https://docs.convex.dev/client/react#fetching-data) in our 58 | [React client](https://docs.convex.dev/client/react). There are also 59 | [Python](https://docs.convex.dev/client/python), 60 | [Rust](https://docs.convex.dev/client/rust), 61 | [ReactNative](https://docs.convex.dev/client/react-native), and 62 | [Node](https://docs.convex.dev/client/javascript) clients, as well as a straightforward 63 | [HTTP API](https://github.com/get-convex/convex-js/blob/main/src/browser/http_client.ts#L40). 64 | 65 | The database support 66 | [NoSQL-style documents](https://docs.convex.dev/database/document-storage) with 67 | [relationships](https://docs.convex.dev/database/document-ids) and 68 | [custom indexes](https://docs.convex.dev/database/indexes/) 69 | (including on fields in nested objects). 70 | 71 | The 72 | [`query`](https://docs.convex.dev/functions/query-functions) and 73 | [`mutation`](https://docs.convex.dev/functions/mutation-functions) server functions have transactional, 74 | low latency access to the database and leverage our 75 | [`v8` runtime](https://docs.convex.dev/functions/runtimes) with 76 | [determinism guardrails](https://docs.convex.dev/functions/runtimes#using-randomness-and-time-in-queries-and-mutations) 77 | to provide the strongest ACID guarantees on the market: 78 | immediate consistency, 79 | serializable isolation, and 80 | automatic conflict resolution via 81 | [optimistic multi-version concurrency control](https://docs.convex.dev/database/advanced/occ) (OCC / MVCC). 82 | 83 | The [`action` server functions](https://docs.convex.dev/functions/actions) have 84 | access to external APIs and enable other side-effects and non-determinism in 85 | either our 86 | [optimized `v8` runtime](https://docs.convex.dev/functions/runtimes) or a more 87 | [flexible `node` runtime](https://docs.convex.dev/functions/runtimes#nodejs-runtime). 88 | 89 | Functions can run in the background via 90 | [scheduling](https://docs.convex.dev/scheduling/scheduled-functions) and 91 | [cron jobs](https://docs.convex.dev/scheduling/cron-jobs). 92 | 93 | Development is cloud-first, with 94 | [hot reloads for server function](https://docs.convex.dev/cli#run-the-convex-dev-server) editing via the 95 | [CLI](https://docs.convex.dev/cli). There is a 96 | [dashbord UI](https://docs.convex.dev/dashboard) to 97 | [browse and edit data](https://docs.convex.dev/dashboard/deployments/data), 98 | [edit environment variables](https://docs.convex.dev/production/environment-variables), 99 | [view logs](https://docs.convex.dev/dashboard/deployments/logs), 100 | [run server functions](https://docs.convex.dev/dashboard/deployments/functions), and more. 101 | 102 | There are built-in features for 103 | [reactive pagination](https://docs.convex.dev/database/pagination), 104 | [file storage](https://docs.convex.dev/file-storage), 105 | [reactive search](https://docs.convex.dev/text-search), 106 | [https endpoints](https://docs.convex.dev/functions/http-actions) (for webhooks), 107 | [streaming import/export](https://docs.convex.dev/database/import-export/), and 108 | [runtime data validation](https://docs.convex.dev/database/schemas#validators) for 109 | [function arguments](https://docs.convex.dev/functions/args-validation) and 110 | [database data](https://docs.convex.dev/database/schemas#schema-validation). 111 | 112 | Everything scales automatically, and it’s [free to start](https://www.convex.dev/plans). 113 | -------------------------------------------------------------------------------- /convex/lucia.ts: -------------------------------------------------------------------------------- 1 | // lucia.ts 2 | import { 3 | Adapter, 4 | KeySchema, 5 | LuciaErrorConstructor, 6 | SessionSchema, 7 | UserSchema, 8 | lucia, 9 | } from "lucia"; 10 | import { DatabaseReader, DatabaseWriter } from "./_generated/server"; 11 | 12 | type SessionId = string; 13 | type UserId = string; 14 | type KeyId = string; 15 | 16 | export function getAuth(db: DatabaseWriter) { 17 | return lucia({ 18 | // We cheat to allow queries to use `getAuth` 19 | adapter: convexAdapter(db), 20 | // TODO: Set the LUCIA_ENVIRONMENT variable to "PROD" 21 | // on your prod deployment's dashboard 22 | env: (process.env.LUCIA_ENVIRONMENT as "PROD" | undefined) ?? "DEV", 23 | getUserAttributes(user: UserSchema) { 24 | return { 25 | _id: user._id, 26 | _creationTime: user._creationTime, 27 | email: user.email, 28 | }; 29 | }, 30 | }); 31 | } 32 | 33 | const convexAdapter = (db: DatabaseWriter) => { 34 | return (LuciaError: LuciaErrorConstructor): Adapter => ({ 35 | async getSessionAndUser( 36 | sessionId: string 37 | ): Promise<[SessionSchema, UserSchema] | [null, null]> { 38 | const session = await getSession(db, sessionId); 39 | if (session === null) { 40 | return [null, null]; 41 | } 42 | const user = await getUser(db, session.user_id); 43 | if (user === null) { 44 | return [null, null]; 45 | } 46 | return [session, user]; 47 | }, 48 | async deleteSession(sessionId: SessionId): Promise { 49 | const session = await getSession(db, sessionId); 50 | if (session === null) { 51 | return; 52 | } 53 | await db.delete(session._id); 54 | }, 55 | async deleteSessionsByUserId(userId: UserId): Promise { 56 | const sessions = await this.getSessionsByUserId(userId); 57 | await Promise.all(sessions.map((session) => db.delete(session._id))); 58 | }, 59 | async getSession(sessionId: SessionId): Promise { 60 | return await getSession(db, sessionId); 61 | }, 62 | async getSessionsByUserId(userId: UserId): Promise { 63 | return await db 64 | .query("sessions") 65 | .withIndex("byUserId", (q) => q.eq("user_id", userId)) 66 | .collect(); 67 | }, 68 | async setSession(session: SessionSchema): Promise { 69 | const { _id, _creationTime, ...data } = session; 70 | await db.insert("sessions", data); 71 | }, 72 | async deleteKeysByUserId(userId: UserId): Promise { 73 | const keys = await db 74 | .query("auth_keys") 75 | .withIndex("byUserId", (q) => q.eq("user_id", userId)) 76 | .collect(); 77 | await Promise.all(keys.map((key) => db.delete(key._id))); 78 | }, 79 | async deleteKey(keyId: KeyId): Promise { 80 | const key = await getKey(db, keyId); 81 | if (key === null) { 82 | return; 83 | } 84 | await db.delete(key._id); 85 | }, 86 | async deleteUser(userId: UserId): Promise { 87 | const user = await getUser(db, userId); 88 | if (user === null) { 89 | return; 90 | } 91 | await db.delete(user._id); 92 | }, 93 | async getKey(keyId: KeyId): Promise { 94 | return await getKey(db, keyId); 95 | }, 96 | async getKeysByUserId(userId: UserId): Promise { 97 | return await db 98 | .query("auth_keys") 99 | .withIndex("byUserId", (q) => q.eq("user_id", userId)) 100 | .collect(); 101 | }, 102 | async getUser(userId: UserId): Promise { 103 | return await getUser(db, userId); 104 | }, 105 | async setKey(key: KeySchema): Promise { 106 | const existingKey = await this.getKey(key.id); 107 | if (existingKey !== null) { 108 | throw new LuciaError("AUTH_DUPLICATE_KEY_ID"); 109 | } 110 | const user = await this.getUser(key.user_id); 111 | if (user === null) { 112 | throw new LuciaError("AUTH_INVALID_USER_ID"); 113 | } 114 | await db.insert("auth_keys", key); 115 | }, 116 | async setUser(user: UserSchema, key: KeySchema | null): Promise { 117 | const { _id, _creationTime, ...data } = user; 118 | await db.insert("users", data); 119 | if (key !== null) { 120 | await this.setKey(key); 121 | } 122 | }, 123 | async updateKey( 124 | keyId: string, 125 | partialKey: Partial 126 | ): Promise { 127 | const key = await getKey(db, keyId); 128 | if (key === null) { 129 | throw new LuciaError("AUTH_INVALID_KEY_ID"); 130 | } 131 | await db.patch(key._id, partialKey); 132 | }, 133 | async updateUser( 134 | userId: string, 135 | partialUser: Partial 136 | ): Promise { 137 | const user = await getUser(db, userId); 138 | if (user === null) { 139 | throw new LuciaError("AUTH_INVALID_USER_ID"); 140 | } 141 | await db.patch(user._id, partialUser); 142 | }, 143 | async updateSession( 144 | sessionId: string, 145 | partialSession: Partial 146 | ): Promise { 147 | const session = await getSession(db, sessionId); 148 | if (session === null) { 149 | throw new LuciaError("AUTH_INVALID_SESSION_ID"); 150 | } 151 | await db.patch(session._id, partialSession); 152 | }, 153 | }); 154 | }; 155 | 156 | async function getSession(db: DatabaseReader, sessionId: string) { 157 | return await db 158 | .query("sessions") 159 | .withIndex("byId", (q) => q.eq("id", sessionId)) 160 | .first(); 161 | } 162 | 163 | async function getUser(db: DatabaseReader, userId: string) { 164 | return await db 165 | .query("users") 166 | .withIndex("byId", (q) => q.eq("id", userId)) 167 | .first(); 168 | } 169 | 170 | async function getKey(db: DatabaseReader, keyId: string) { 171 | return await db 172 | .query("auth_keys") 173 | .withIndex("byId", (q) => q.eq("id", keyId)) 174 | .first(); 175 | } 176 | 177 | export type Auth = ReturnType; 178 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | ActionCtx as GenericActionCtx, 18 | MutationCtx as GenericMutationCtx, 19 | QueryCtx as GenericQueryCtx, 20 | DatabaseReader as GenericDatabaseReader, 21 | DatabaseWriter as GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder<"public">; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder<"internal">; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Convex, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------