├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── grafbase
├── .env.example
└── schema.graphql
├── next-auth.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── og.png
└── sent.wav
├── src
├── components
│ ├── apollo-provider-wrapper.tsx
│ ├── header.tsx
│ ├── message-list.tsx
│ ├── message.tsx
│ └── new-message-form.tsx
├── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ └── auth
│ │ │ ├── [...nextauth].ts
│ │ │ └── token.ts
│ └── index.tsx
└── styles
│ └── globals.css
├── tailwind.config.js
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # This must be your local or Grafbase production API endpoint
2 | # Click "Connect" inside your Grafbase Project Dashboard to get the URL
3 | NEXT_PUBLIC_GRAFBASE_API_URL=http://localhost:4000/graphql
4 |
5 | # The NEXTAUTH_SECRET must match that set inside the grafbase/.env.
6 | # You can generate one here: https://generate-secret.vercel.app
7 | NEXTAUTH_SECRET=
8 |
9 | # Create a new GitHub app in your account settings to obtain these values
10 | GITHUB_CLIENT_ID=
11 | GITHUB_CLIENT_SECRET=
12 |
--------------------------------------------------------------------------------
/.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 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 | grafbase/.env
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chatbase
2 |
3 | Realtime chat using GraphQL Live Queries, Next.js and NextAuth.js — [tutorial](https://grafbase.com/guides/how-to-build-a-real-time-chat-app-with-nextjs-graphql-and-server-sent-events)
4 |
5 | 
6 |
7 | ## Tools used
8 |
9 | - NextAuth.js
10 | - Next.js
11 | - Apollo Client
12 | - Grafbase
13 | - Server-Sent Events
14 | - GraphQL Live Queries
15 | - GraphQL
16 | - Tailwind CSS
17 |
18 | ## Local Development
19 |
20 | 1. `npm install`
21 | 2. Create a [GitHub OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) with your app details for development purposes. Make sure to set `Authorization callback URL` to `http://localhost:3000/api/auth/callback/github`
22 | 3. `cp .env.example .env` and add values for `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` from step 2.
23 | 4. [Generate a secret value](https://generate-secret.vercel.app) for `NEXTAUTH_SECRET` and add it to `.env`
24 | 5. `cp grafbase/.env.example grafbase/.env`
25 | 6. Add the same `NEXTAUTH_SECRET` to `grafbase/.env`
26 | 7. `npx grafbase dev`
27 | 8. `npm run dev`
28 |
29 | ## Deploy to Production
30 |
31 | 1. Fork and Push this repo to GitHub
32 | 2. [Create an account](https://grafbase.com) with Grafbase
33 | 3. Create new project with Grafbase and connect your forked repo
34 | 4. Add environment variable `NEXTAUTH_SECRET` during project creation
35 | 5. Create a [GitHub OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) with your app details for production purposes. Make sure to set `Authorization callback URL` to `[YOUR_DESIRED_VERCEL_DOMAIN]/api/auth/callback/github`
36 | 6. Deploy to Vercel and add `.env` values (`NEXT_PUBLIC_GRAFBASE_API_URL`\*, `NEXTAUTH_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`)
37 |
38 | \* `NEXT_PUBLIC_GRAFBASE_URL` is your production API endpoint. You can find this from the **Connect** modal in your [project dashboard](https://grafbase.com/dashboard).
39 |
--------------------------------------------------------------------------------
/grafbase/.env.example:
--------------------------------------------------------------------------------
1 | # This value must be the same as the root .env
2 | # You can generate one here: https://generate-secret.vercel.app
3 | NEXTAUTH_SECRET=
--------------------------------------------------------------------------------
/grafbase/schema.graphql:
--------------------------------------------------------------------------------
1 | schema
2 | @auth(
3 | providers: [
4 | { type: jwt, issuer: "nextauth", secret: "{{ env.NEXTAUTH_SECRET }}" }
5 | ]
6 | rules: [{ allow: private }]
7 | ) {
8 | query: Query
9 | }
10 |
11 | type Message @model {
12 | username: String!
13 | avatar: URL
14 | body: String!
15 | likes: Int @default(value: 0)
16 | dislikes: Int @default(value: 0)
17 | }
18 |
--------------------------------------------------------------------------------
/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import type { DefaultSession, JWT } from "next-auth";
2 | import NextAuth from "next-auth";
3 |
4 | declare module "next-auth" {
5 | interface Session {
6 | username?: string;
7 | user: {
8 | username?: string;
9 | } & DefaultSession["user"];
10 | }
11 | interface Profile {
12 | login?: string;
13 | }
14 | }
15 |
16 | declare module "next-auth/jwt" {
17 | interface JWT {
18 | username?: string;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: ["avatars.githubusercontent.com"],
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatbase",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@apollo/client": "3.7.9",
13 | "@grafbase/apollo-link": "1.1.0",
14 | "@types/node": "18.14.5",
15 | "@types/react": "18.0.28",
16 | "@types/react-dom": "18.0.11",
17 | "date-fns": "2.30.0",
18 | "eslint": "8.35.0",
19 | "eslint-config-next": "13.2.3",
20 | "graphql": "16.6.0",
21 | "jsonwebtoken": "9.0.0",
22 | "next": "13.2.3",
23 | "next-auth": "4.20.1",
24 | "react": "18.2.0",
25 | "react-dom": "18.2.0",
26 | "react-intersection-observer": "9.4.3",
27 | "typescript": "4.9.5",
28 | "use-sound": "4.0.1"
29 | },
30 | "devDependencies": {
31 | "@types/jsonwebtoken": "9.0.1",
32 | "autoprefixer": "10.4.13",
33 | "postcss": "8.4.21",
34 | "tailwindcss": "3.2.7"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notrab/chatbase/f0a5d02320ac602a1b7f4b4ad155a2d4ec79b8a4/public/og.png
--------------------------------------------------------------------------------
/public/sent.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/notrab/chatbase/f0a5d02320ac602a1b7f4b4ad155a2d4ec79b8a4/public/sent.wav
--------------------------------------------------------------------------------
/src/components/apollo-provider-wrapper.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 | import { useMemo } from "react";
3 | import {
4 | ApolloClient,
5 | ApolloProvider,
6 | HttpLink,
7 | InMemoryCache,
8 | split,
9 | from,
10 | } from "@apollo/client";
11 | import { SSELink, isLiveQuery } from "@grafbase/apollo-link";
12 | import { getOperationAST } from "graphql";
13 | import { setContext } from "@apollo/client/link/context";
14 |
15 | const httpLink = new HttpLink({
16 | uri: process.env.NEXT_PUBLIC_GRAFBASE_API_URL,
17 | });
18 |
19 | const sseLink = new SSELink({
20 | uri: process.env.NEXT_PUBLIC_GRAFBASE_API_URL!,
21 | });
22 |
23 | export const ApolloProviderWrapper = ({ children }: PropsWithChildren) => {
24 | const client = useMemo(() => {
25 | const authMiddleware = setContext(async (_, { headers }) => {
26 | const { token } = await fetch("/api/auth/token").then((res) =>
27 | res.json()
28 | );
29 |
30 | return {
31 | headers: {
32 | ...headers,
33 | authorization: `Bearer ${token}`,
34 | },
35 | };
36 | });
37 |
38 | return new ApolloClient({
39 | link: from([
40 | authMiddleware,
41 | split(
42 | ({ query, operationName, variables }) =>
43 | isLiveQuery(getOperationAST(query, operationName), variables),
44 | sseLink,
45 | httpLink
46 | ),
47 | ]),
48 | cache: new InMemoryCache(),
49 | });
50 | }, []);
51 |
52 | return
12 |
17 |
35 |
36 | Chatbase
37 |
Fetching most recent chat messages.
49 |Something went wrong. Refresh to try again.
55 | ); 56 | 57 | return ( 58 |64 | {differenceInHours(new Date(), new Date(message.createdAt)) >= 1 65 | ? formatRelative(new Date(message.createdAt), new Date()) 66 | : formatDistance(new Date(message.createdAt), new Date(), { 67 | addSuffix: true, 68 | })} 69 |
70 |33 | Sign in with GitHub to join the chat! 34 |
35 |36 | 42 | Powered by Grafbase & GraphQL Live Queries 43 | 44 |
45 | > 46 | )} 47 |