├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (common) │ ├── BackLink.tsx │ ├── Posts.tsx │ └── PostsScrollView.tsx ├── AllPosts.tsx ├── Composer.tsx ├── ConvexClientProvider.tsx ├── SignInOrComposer.tsx ├── [username] │ ├── UserPosts.tsx │ ├── UserProfile.tsx │ └── page.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx ├── post │ └── [id] │ │ └── page.tsx └── useStoreUserEffect.ts ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── auth.config.js ├── posts.ts ├── schema.ts ├── shared.ts ├── tsconfig.json └── users.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── screenshot.png ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convex Next.js App Router Demo 2 | 3 | This example app showcases Convex backend powering a Next.js app. 4 | 5 | ![Screenshot of the app](./screenshot.png) 6 | 7 | The app is a realtime social network with a limit of 20 characters per post (who needs more?). There are three pages: 8 | 9 | - The home feed and author feed pages are dynamic and use client components. You can sign in with Github (via Clerk) to make new posts on the home feed. 10 | - The single post page is using server components and SSR to render a post. 11 | 12 | The demo includes server-side validation and rate limiting. 13 | 14 | See it live here: https://convex-nextjs-app-router-demo.vercel.app/ 15 | 16 | ## Setting up 17 | 18 | Run: 19 | 20 | ```bash 21 | npm install 22 | npm run dev 23 | ``` 24 | 25 | This will guide you through making a Convex project. 26 | 27 | Open [http://localhost:3000](http://localhost:3000) in your browser to see the result. 28 | 29 | ### Environment variables 30 | 31 | To test signing in with Clerk and making posts, follow these steps: 32 | 33 | 1. Follow the first 3 steps from the [Get Started](https://docs.convex.dev/auth/clerk) guide for Clerk. 34 | 2. Instead of pasting your _Issuer URL_ to the config file add an environment variable `CLERK_JWT_ISSUER_DOMAIN` with the _Issuer URL_ to your Deployment Settings on the Convex [dashboard](https://dashboard.convex.dev/) 35 | 3. In your `.env.local` file add a variable `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` with your Publishable Key (see step 7 of the [Get Started](https://docs.convex.dev/auth/clerk) guide) 36 | 37 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 38 | 39 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 40 | 41 | # What is Convex? 42 | 43 | [Convex](https://convex.dev) is a hosted backend platform with a 44 | built-in database that lets you write your 45 | [database schema](https://docs.convex.dev/database/schemas) and 46 | [server functions](https://docs.convex.dev/functions) in 47 | [TypeScript](https://docs.convex.dev/typescript). Server-side database 48 | [queries](https://docs.convex.dev/functions/query-functions) automatically 49 | [cache](https://docs.convex.dev/functions/query-functions#caching--reactivity) and 50 | [subscribe](https://docs.convex.dev/client/react#reactivity) to data, powering a 51 | [realtime `useQuery` hook](https://docs.convex.dev/client/react#fetching-data) in our 52 | [React client](https://docs.convex.dev/client/react). There are also 53 | [Python](https://docs.convex.dev/client/python), 54 | [Rust](https://docs.convex.dev/client/rust), 55 | [ReactNative](https://docs.convex.dev/client/react-native), and 56 | [Node](https://docs.convex.dev/client/javascript) clients, as well as a straightforward 57 | [HTTP API](https://github.com/get-convex/convex-js/blob/main/src/browser/http_client.ts#L40). 58 | 59 | The database support 60 | [NoSQL-style documents](https://docs.convex.dev/database/document-storage) with 61 | [relationships](https://docs.convex.dev/database/document-ids) and 62 | [custom indexes](https://docs.convex.dev/database/indexes/) 63 | (including on fields in nested objects). 64 | 65 | The 66 | [`query`](https://docs.convex.dev/functions/query-functions) and 67 | [`mutation`](https://docs.convex.dev/functions/mutation-functions) server functions have transactional, 68 | low latency access to the database and leverage our 69 | [`v8` runtime](https://docs.convex.dev/functions/runtimes) with 70 | [determinism guardrails](https://docs.convex.dev/functions/runtimes#using-randomness-and-time-in-queries-and-mutations) 71 | to provide the strongest ACID guarantees on the market: 72 | immediate consistency, 73 | serializable isolation, and 74 | automatic conflict resolution via 75 | [optimistic multi-version concurrency control](https://docs.convex.dev/database/advanced/occ) (OCC / MVCC). 76 | 77 | The [`action` server functions](https://docs.convex.dev/functions/actions) have 78 | access to external APIs and enable other side-effects and non-determinism in 79 | either our 80 | [optimized `v8` runtime](https://docs.convex.dev/functions/runtimes) or a more 81 | [flexible `node` runtime](https://docs.convex.dev/functions/runtimes#nodejs-runtime). 82 | 83 | Functions can run in the background via 84 | [scheduling](https://docs.convex.dev/scheduling/scheduled-functions) and 85 | [cron jobs](https://docs.convex.dev/scheduling/cron-jobs). 86 | 87 | Development is cloud-first, with 88 | [hot reloads for server function](https://docs.convex.dev/cli#run-the-convex-dev-server) editing via the 89 | [CLI](https://docs.convex.dev/cli). There is a 90 | [dashbord UI](https://docs.convex.dev/dashboard) to 91 | [browse and edit data](https://docs.convex.dev/dashboard/deployments/data), 92 | [edit environment variables](https://docs.convex.dev/production/environment-variables), 93 | [view logs](https://docs.convex.dev/dashboard/deployments/logs), 94 | [run server functions](https://docs.convex.dev/dashboard/deployments/functions), and more. 95 | 96 | There are built-in features for 97 | [reactive pagination](https://docs.convex.dev/database/pagination), 98 | [file storage](https://docs.convex.dev/file-storage), 99 | [reactive search](https://docs.convex.dev/text-search), 100 | [https endpoints](https://docs.convex.dev/functions/http-actions) (for webhooks), 101 | [streaming import/export](https://docs.convex.dev/database/import-export/), and 102 | [runtime data validation](https://docs.convex.dev/database/schemas#validators) for 103 | [function arguments](https://docs.convex.dev/functions/args-validation) and 104 | [database data](https://docs.convex.dev/database/schemas#schema-validation). 105 | 106 | Everything scales automatically, and it’s [free to start](https://www.convex.dev/plans). 107 | -------------------------------------------------------------------------------- /app/(common)/BackLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export function BackLink() { 4 | return {"<"} Back to feed; 5 | } 6 | -------------------------------------------------------------------------------- /app/(common)/Posts.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from "@/convex/posts"; 2 | import dayjs from "dayjs"; 3 | import relativeTime from "dayjs/plugin/relativeTime"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | 7 | dayjs.extend(relativeTime); 8 | 9 | export function Posts(props: { posts: Post[] }) { 10 | return ( 11 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/(common)/PostsScrollView.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | PaginatedQueryArgs, 5 | PaginatedQueryReference, 6 | usePaginatedQuery, 7 | } from "convex/react"; 8 | import { useEffect } from "react"; 9 | import { Posts } from "./Posts"; 10 | 11 | export function PostsScrollView({ 12 | query, 13 | args, 14 | }: { 15 | query: Query; 16 | args: PaginatedQueryArgs; 17 | }) { 18 | const { 19 | results: posts, 20 | status, 21 | loadMore, 22 | } = usePaginatedQuery(query, args, { initialNumItems: 5 }); 23 | 24 | useEffect(() => { 25 | const handleScroll = () => { 26 | const page = document.documentElement; 27 | const closeToBottom = 28 | page.scrollHeight - page.scrollTop - page.clientHeight < 100; 29 | if (closeToBottom && status === "CanLoadMore") { 30 | loadMore(5); 31 | } 32 | }; 33 | handleScroll(); 34 | document.addEventListener("scroll", handleScroll); 35 | return () => document.removeEventListener("scroll", handleScroll); 36 | }, [status, loadMore]); 37 | 38 | return ; 39 | } 40 | -------------------------------------------------------------------------------- /app/AllPosts.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/convex/_generated/api"; 4 | import { PostsScrollView } from "./(common)/PostsScrollView"; 5 | 6 | export function AllPosts() { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /app/Composer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/convex/_generated/api"; 4 | import { CHARACTER_LIMIT } from "@/convex/shared"; 5 | import { UserButton } from "@clerk/clerk-react"; 6 | import { useMutation } from "convex/react"; 7 | import { useFormik } from "formik"; 8 | import useStoreUserEffect from "./useStoreUserEffect"; 9 | 10 | export function Composer() { 11 | const userId = useStoreUserEffect(); 12 | const createPost = useMutation(api.posts.create); 13 | const formik = useFormik({ 14 | initialValues: { 15 | text: "", 16 | }, 17 | onSubmit: async (values) => { 18 | try { 19 | await createPost({ ...values }); 20 | formik.resetForm(); 21 | } catch (e) { 22 | formik.setErrors({ text: "Couldn't send this post, try later" }); 23 | } 24 | }, 25 | }); 26 | const charCount = formik.values.text.length; 27 | const isOverLimit = charCount > CHARACTER_LIMIT; 28 | return ( 29 |
30 |
31 |
32 | 33 |
34 |
35 | 41 | 49 |
50 |
51 | {formik.errors.text !== undefined ? ( 52 |
{formik.errors.text}
53 | ) : null} 54 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /app/ConvexClientProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | import { ConvexReactClient } from "convex/react"; 5 | import { ConvexProviderWithClerk } from "convex/react-clerk"; 6 | import { ClerkProvider, useAuth } from "@clerk/clerk-react"; 7 | 8 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 9 | 10 | export default function ConvexClientProvider({ 11 | children, 12 | }: { 13 | children: ReactNode; 14 | }) { 15 | return ( 16 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/SignInOrComposer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignInButton } from "@clerk/clerk-react"; 4 | import { useConvexAuth } from "convex/react"; 5 | import { Composer } from "./Composer"; 6 | 7 | export function SignInOrComposer() { 8 | const { isLoading, isAuthenticated } = useConvexAuth(); 9 | return isAuthenticated ? ( 10 | 11 | ) : ( 12 |
13 |
{isLoading ? : }
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/[username]/UserPosts.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/convex/_generated/api"; 4 | import { PostsScrollView } from "../(common)/PostsScrollView"; 5 | 6 | export function UserPosts({ username }: { username: string }) { 7 | return ( 8 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/[username]/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "@/convex/_generated/api"; 4 | import { useQuery } from "convex/react"; 5 | import Image from "next/image"; 6 | 7 | export function UserProfile({ username }: { username: string }) { 8 | const user = useQuery(api.users.get, { username }); 9 | return user == null ? null : ( 10 |
11 | profile pic 12 |

{user.name}

13 |
14 |

@{user.username}

· 15 |

16 | {user.numPosts} Post{user.numPosts === 1 ? "" : "s"} 17 |

18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/[username]/page.tsx: -------------------------------------------------------------------------------- 1 | import { BackLink } from "../(common)/BackLink"; 2 | import { UserPosts } from "./UserPosts"; 3 | import { UserProfile } from "./UserProfile"; 4 | 5 | export default function Profile({ params }: { params: { username: string } }) { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-nextjs-app-router-demo/32f91abe58cd302e4fb9930875c26a369d77697c/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | main { 6 | @apply mx-auto w-[600px]; 7 | } 8 | 9 | ul { 10 | @apply border-l border-r border-t; 11 | } 12 | 13 | li { 14 | @apply p-4 border-b flex gap-2; 15 | } 16 | 17 | li > div { 18 | @apply flex flex-col; 19 | } 20 | 21 | li > div > div { 22 | @apply flex gap-1; 23 | } 24 | 25 | li .name { 26 | @apply font-bold; 27 | } 28 | 29 | li .username { 30 | @apply text-slate-500; 31 | } 32 | 33 | li .time { 34 | @apply text-slate-600; 35 | } 36 | 37 | .composer { 38 | @apply border-r border-l; 39 | } 40 | 41 | .composer > div:first-child { 42 | @apply flex p-4 gap-2; 43 | } 44 | 45 | .composer form { 46 | @apply flex gap-2 flex-grow; 47 | } 48 | 49 | input[type="text"] { 50 | @apply w-full py-2 px-4; 51 | } 52 | 53 | button, 54 | .composer button[type="submit"] { 55 | @apply rounded bg-slate-200 py-2 px-4; 56 | } 57 | 58 | button[disabled], 59 | .composer button[type="submit"][disabled] { 60 | @apply text-slate-400; 61 | } 62 | 63 | .composer .characterLimit { 64 | @apply h-1 bg-gray-300; 65 | } 66 | 67 | .composer .characterLimit.characterLimitOver { 68 | @apply bg-red-400; 69 | } 70 | 71 | .composer .error { 72 | @apply text-red-400 py-2 px-4; 73 | } 74 | 75 | ul li img { 76 | @apply w-10 h-10 rounded-full; 77 | } 78 | 79 | .cl-avatarBox { 80 | @apply w-[2.5rem] h-[2.5rem]; 81 | } 82 | 83 | .authorButton { 84 | @apply min-w-[2.5rem] min-h-[2.5rem]; 85 | } 86 | 87 | .profile { 88 | @apply border-r border-l p-4 bg-gradient-to-b from-slate-300 to-white; 89 | } 90 | 91 | .profile > div { 92 | @apply flex gap-1; 93 | } 94 | 95 | .profile h1 { 96 | @apply font-bold text-2xl; 97 | } 98 | 99 | .profile h2 { 100 | @apply text-slate-500; 101 | } 102 | 103 | .profile img { 104 | @apply w-20 h-20 rounded-full border-2 border-white; 105 | } 106 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import ConvexClientProvider from "./ConvexClientProvider"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const runtime = "edge"; 9 | 10 | export const metadata: Metadata = { 11 | title: "Next.js + Convex", 12 | description: "Generated by create next app and using Convex", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { AllPosts } from "./AllPosts"; 2 | import { SignInOrComposer } from "./SignInOrComposer"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/post/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { BackLink } from "@/app/(common)/BackLink"; 2 | import { Posts } from "@/app/(common)/Posts"; 3 | import { api } from "@/convex/_generated/api"; 4 | import { Id } from "@/convex/_generated/dataModel"; 5 | import { ConvexHttpClient } from "convex/browser"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | 9 | export default async function Post({ params }: { params: { id: string } }) { 10 | const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 11 | const post = await client.query(api.posts.get, { 12 | id: params.id as Id<"posts">, 13 | }); 14 | return ( 15 |
16 | 17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/useStoreUserEffect.ts: -------------------------------------------------------------------------------- 1 | import { useUser } from "@clerk/clerk-react"; 2 | import { useConvexAuth } from "convex/react"; 3 | import { useEffect, useState } from "react"; 4 | import { useMutation } from "convex/react"; 5 | import { api } from "../convex/_generated/api"; 6 | import { Id } from "../convex/_generated/dataModel"; 7 | 8 | export default function useStoreUserEffect() { 9 | const { isAuthenticated } = useConvexAuth(); 10 | const { user } = useUser(); 11 | const [userId, setUserId] = useState | null>(null); 12 | const storeUser = useMutation(api.users.store); 13 | useEffect(() => { 14 | if (!isAuthenticated) { 15 | return; 16 | } 17 | async function createUser() { 18 | const id = await storeUser(); 19 | setUserId(id); 20 | } 21 | createUser(); 22 | return () => setUserId(null); 23 | }, [isAuthenticated, storeUser, user?.id]); 24 | return userId; 25 | } 26 | -------------------------------------------------------------------------------- /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/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.7.1. 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 posts from "../posts.js"; 18 | import type * as shared from "../shared.js"; 19 | import type * as users from "../users.js"; 20 | 21 | /** 22 | * A utility for referencing Convex functions in your app's API. 23 | * 24 | * Usage: 25 | * ```js 26 | * const myFunctionReference = api.myModule.myFunction; 27 | * ``` 28 | */ 29 | declare const fullApi: ApiFromModules<{ 30 | posts: typeof posts; 31 | shared: typeof shared; 32 | users: typeof users; 33 | }>; 34 | export declare const api: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | >; 38 | export declare const internal: FilterApi< 39 | typeof fullApi, 40 | FunctionReference 41 | >; 42 | -------------------------------------------------------------------------------- /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.7.1. 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/_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.7.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | DataModelFromSchemaDefinition, 14 | DocumentByName, 15 | TableNamesInDataModel, 16 | SystemTableNames, 17 | } from "convex/server"; 18 | import type { GenericId } from "convex/values"; 19 | import schema from "../schema.js"; 20 | 21 | /** 22 | * The names of all of your Convex tables. 23 | */ 24 | export type TableNames = TableNamesInDataModel; 25 | 26 | /** 27 | * The type of a document stored in Convex. 28 | * 29 | * @typeParam TableName - A string literal type of the table name (like "users"). 30 | */ 31 | export type Doc = DocumentByName< 32 | DataModel, 33 | TableName 34 | >; 35 | 36 | /** 37 | * An identifier for a document in Convex. 38 | * 39 | * Convex documents are uniquely identified by their `Id`, which is accessible 40 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 41 | * 42 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 43 | * 44 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 45 | * strings when type checking. 46 | * 47 | * @typeParam TableName - A string literal type of the table name (like "users"). 48 | */ 49 | export type Id = 50 | GenericId; 51 | 52 | /** 53 | * A type describing your Convex data model. 54 | * 55 | * This type includes information about what tables you have, the type of 56 | * documents stored in those tables, and the indexes defined on them. 57 | * 58 | * This type is used to parameterize methods like `queryGeneric` and 59 | * `mutationGeneric` to make them type-safe. 60 | */ 61 | export type DataModel = DataModelFromSchemaDefinition; 62 | -------------------------------------------------------------------------------- /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.7.1. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | GenericActionCtx, 18 | GenericMutationCtx, 19 | GenericQueryCtx, 20 | GenericDatabaseReader, 21 | 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; 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; 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 | -------------------------------------------------------------------------------- /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.7.1. 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 | -------------------------------------------------------------------------------- /convex/auth.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | providers: [ 3 | { 4 | domain: process.env.CLERK_JWT_ISSUER_DOMAIN, 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /convex/posts.ts: -------------------------------------------------------------------------------- 1 | import { asyncMap } from "modern-async"; 2 | import { paginationOptsValidator } from "convex/server"; 3 | import { v } from "convex/values"; 4 | import { QueryCtx, mutation, query } from "./_generated/server"; 5 | import { Doc } from "./_generated/dataModel"; 6 | import { getUser } from "./users"; 7 | import { CHARACTER_LIMIT } from "./shared"; 8 | 9 | export const all = query({ 10 | args: { paginationOpts: paginationOptsValidator }, 11 | handler: async (ctx, args) => { 12 | const result = await ctx.db 13 | .query("posts") 14 | .order("desc") 15 | .paginate(args.paginationOpts); 16 | 17 | return { ...result, page: await enrichPosts(ctx, result.page) }; 18 | }, 19 | }); 20 | 21 | export const forAuthor = query({ 22 | args: { 23 | authorUserName: v.string(), 24 | paginationOpts: paginationOptsValidator, 25 | }, 26 | handler: async (ctx, args) => { 27 | const author = await getUser(ctx, args.authorUserName); 28 | if (author === null) { 29 | return { page: [], isDone: true, continueCursor: "" }; 30 | } 31 | const result = await ctx.db 32 | .query("posts") 33 | .withIndex("authorId", (q) => q.eq("authorId", author._id)) 34 | .order("desc") 35 | .paginate(args.paginationOpts); 36 | 37 | return { ...result, page: await enrichPosts(ctx, result.page) }; 38 | }, 39 | }); 40 | 41 | export type Post = NonNullable>>; 42 | 43 | export const get = query({ 44 | args: { id: v.id("posts") }, 45 | handler: async (ctx, args) => { 46 | const post = await ctx.db.get(args.id); 47 | if (post === null) { 48 | return null; 49 | } 50 | return await enrichPost(ctx, post); 51 | }, 52 | }); 53 | 54 | async function enrichPosts(ctx: QueryCtx, posts: Doc<"posts">[]) { 55 | return await asyncMap(posts, (post) => enrichPost(ctx, post)); 56 | } 57 | 58 | async function enrichPost(ctx: QueryCtx, post: Doc<"posts">) { 59 | const author = await ctx.db.get(post.authorId); 60 | if (author === null) { 61 | return null; 62 | } 63 | return { ...post, author }; 64 | } 65 | 66 | export const create = mutation({ 67 | args: { text: v.string() }, 68 | handler: async (ctx, { text }) => { 69 | const identity = await ctx.auth.getUserIdentity(); 70 | if (identity === null) { 71 | throw new Error("Called createPost without being authenticated"); 72 | } 73 | const author = await ctx.db 74 | .query("users") 75 | .withIndex("tokenIdentifier", (q) => 76 | q.eq("tokenIdentifier", identity.tokenIdentifier) 77 | ) 78 | .unique(); 79 | 80 | if (author === null) { 81 | throw new Error("User not found"); 82 | } 83 | 84 | if (text.length <= 0 || text.length > CHARACTER_LIMIT) { 85 | throw new Error("Message is too damn long! (or empty)"); 86 | } 87 | 88 | const numSentRecently = ( 89 | await ctx.db 90 | .query("posts") 91 | .withIndex("authorId", (q) => 92 | q 93 | .eq("authorId", author._id) 94 | .gte("_creationTime", Date.now() - 1000 * 60) 95 | ) 96 | .take(3) 97 | ).length; 98 | 99 | if (numSentRecently >= 3) { 100 | throw new Error("Too fast, slow down!"); 101 | } 102 | 103 | await ctx.db.insert("posts", { authorId: author._id, text }); 104 | // Instead of computing the number of tweets when a profile 105 | // is loaded, we "denormalize" the data and increment 106 | // a counter - this is safe thanks to Convex's ACID properties! 107 | await ctx.db.patch(author._id, { numPosts: author.numPosts + 1 }); 108 | }, 109 | }); 110 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | users: defineTable({ 6 | // Unique identifier from the auth provider 7 | tokenIdentifier: v.string(), 8 | name: v.string(), 9 | username: v.string(), 10 | pictureUrl: v.string(), 11 | numPosts: v.number(), 12 | }) 13 | .index("tokenIdentifier", ["tokenIdentifier"]) 14 | .index("username", ["username"]), 15 | posts: defineTable({ 16 | authorId: v.id("users"), 17 | text: v.string(), 18 | }).index("authorId", ["authorId"]), 19 | }); 20 | -------------------------------------------------------------------------------- /convex/shared.ts: -------------------------------------------------------------------------------- 1 | export const CHARACTER_LIMIT = 20; 2 | -------------------------------------------------------------------------------- /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 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /convex/users.ts: -------------------------------------------------------------------------------- 1 | import { v } from "convex/values"; 2 | import { QueryCtx, mutation, query } from "./_generated/server"; 3 | 4 | export const store = mutation({ 5 | args: {}, 6 | handler: async (ctx) => { 7 | const identity = await ctx.auth.getUserIdentity(); 8 | if (!identity) { 9 | throw new Error("Called storeUser without authentication present"); 10 | } 11 | const user = await getUser(ctx, identity.nickname!); 12 | if (user !== null) { 13 | if ( 14 | user.name !== identity.name || 15 | user.username !== identity.nickname || 16 | user.pictureUrl !== identity.pictureUrl || 17 | user.tokenIdentifier !== identity.tokenIdentifier 18 | ) { 19 | await ctx.db.patch(user._id, { 20 | tokenIdentifier: identity.tokenIdentifier, 21 | name: identity.name, 22 | username: identity.nickname, 23 | pictureUrl: identity.pictureUrl, 24 | }); 25 | } 26 | return user._id; 27 | } 28 | // If it's a new identity, create a new `User`. 29 | return await ctx.db.insert("users", { 30 | tokenIdentifier: identity.tokenIdentifier, 31 | name: identity.name!, 32 | username: identity.nickname!, 33 | pictureUrl: identity.pictureUrl!, 34 | numPosts: 0, 35 | }); 36 | }, 37 | }); 38 | 39 | export const get = query({ 40 | args: { 41 | username: v.string(), 42 | }, 43 | handler: async (ctx, args) => { 44 | return await getUser(ctx, args.username); 45 | }, 46 | }); 47 | 48 | export async function getUser(ctx: QueryCtx, username: string) { 49 | return await ctx.db 50 | .query("users") 51 | .withIndex("username", (q) => q.eq("username", username)) 52 | .unique(); 53 | } 54 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "img.clerk.com", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | module.exports = nextConfig; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-nextjs-app-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm-run-all dev:init --parallel dev:backend dev:frontend", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "dev:backend": "convex dev", 11 | "dev:frontend": "next dev", 12 | "dev:init": "convex dev --until-success" 13 | }, 14 | "dependencies": { 15 | "@clerk/clerk-react": "^4.30.3", 16 | "@types/node": "20.11.0", 17 | "@types/react": "18.2.47", 18 | "@types/react-dom": "18.2.18", 19 | "autoprefixer": "10.4.16", 20 | "convex": "^1.7.1", 21 | "dayjs": "^1.11.10", 22 | "eslint": "8.56.0", 23 | "eslint-config-next": "14.0.4", 24 | "formik": "^2.4.5", 25 | "modern-async": "^2.0.0", 26 | "next": "14.0.4", 27 | "postcss": "8.4.33", 28 | "react": "18.2.0", 29 | "react-dom": "18.2.0", 30 | "tailwindcss": "3.4.1", 31 | "typescript": "5.3.3" 32 | }, 33 | "devDependencies": { 34 | "@types/async": "^3.2.24", 35 | "npm-run-all": "^4.1.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/convex-nextjs-app-router-demo/32f91abe58cd302e4fb9930875c26a369d77697c/screenshot.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------