├── .env.example
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmrc
├── .prettierignore
├── .vscode
├── cspell.json
├── extensions.json
└── settings.json
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── patches
└── @tanstack__react-query@4.14.5.patch
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── prisma
└── schema.prisma
├── public
└── favicon.ico
├── src
├── app
│ ├── api
│ │ ├── auth
│ │ │ └── [...nextauth]
│ │ │ │ └── route.ts
│ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ ├── post
│ │ └── [slug]
│ │ │ └── page.tsx
│ ├── posts
│ │ └── create
│ │ │ ├── create-post-form.tsx
│ │ │ └── page.tsx
│ └── profile
│ │ └── page.tsx
├── auth
│ ├── adapters
│ │ └── kysely.ts
│ ├── client
│ │ └── index.ts
│ ├── options.ts
│ └── server
│ │ └── index.ts
├── components
│ ├── icons.tsx
│ ├── main-dropdown-menu.tsx
│ ├── main-nav
│ │ ├── main-nav-inner.tsx
│ │ └── main-nav.tsx
│ ├── mobile-nav.tsx
│ ├── posts-table.tsx
│ ├── sign-in-options.tsx
│ ├── theme-provider.tsx
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── hover-card.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── lib
│ │ └── utils.ts
│ │ ├── navigation-menu.tsx
│ │ ├── popover.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ └── tooltip.tsx
├── config
│ ├── docs.ts
│ └── site.ts
├── lib
│ └── kysely-db.ts
├── server
│ ├── auth.ts
│ ├── context.ts
│ ├── env.js
│ ├── routers
│ │ ├── _app.ts
│ │ └── example.ts
│ └── trpc.ts
├── shared
│ ├── hydration.ts
│ ├── server-rsc
│ │ ├── get-user.tsx
│ │ └── trpc.ts
│ └── utils.ts
└── trpc
│ ├── @trpc
│ └── next-layout
│ │ ├── create-hydrate-client.tsx
│ │ ├── create-trpc-next-layout.ts
│ │ ├── index.ts
│ │ └── local-storage.ts
│ └── client
│ ├── hydrate-client.tsx
│ └── trpc-client.tsx
├── tailwind.config.cjs
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # Since .env is gitignored, you can use .env.example to build a new `.env` file when you clone the repo.
2 | # Keep this file up-to-date when you add new variables to \`.env\`.
3 |
4 | # This file will be committed to version control, so make sure not to have any secrets in it.
5 | # If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
6 |
7 | # We use dotenv to load Prisma from Next.js' .env file
8 | # @see https://www.prisma.io/docs/reference/database-reference/connection-urls
9 | # You must use a Planetscale database. This repo relies on the @planetscale/database driver.
10 | DATABASE_URL=
11 |
12 | # @see https://next-auth.js.org/configuration/options#nextauth_url
13 | NEXTAUTH_URL='http://localhost:3000'
14 |
15 | # You can generate the secret via 'openssl rand -base64 32' on Unix
16 | # @see https://next-auth.js.org/configuration/options#secret
17 | # this is the production secret
18 | AUTH_SECRET='fDaMGatBRGilsXXhYn0+KWA6XO0ksfzTGZVzFcesB9M='
19 |
20 | # @see https://next-auth.js.org/providers/discord
21 | # DISCORD_CLIENT_ID=
22 | # DISCORD_CLIENT_SECRET=
23 |
24 | GITHUB_ID=
25 | GITHUB_SECRET=
26 | GOOGLE_CLIENT_ID=
27 | GOOGLE_CLIENT_SECRET=
28 |
29 | NODE_VERSION=14
30 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /next.config.js
2 | /src/server/env.js
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser", // Specifies the ESLint parser
3 | "extends": [
4 | //
5 | "next/core-web-vitals",
6 | "plugin:@typescript-eslint/recommended"
7 | ],
8 | "parserOptions": {
9 | "project": "tsconfig.json",
10 | "ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
11 | "sourceType": "module" // Allows for the use of imports
12 | },
13 | "rules": {
14 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
15 | "@typescript-eslint/explicit-function-return-type": "off",
16 | "@typescript-eslint/explicit-module-boundary-types": "off",
17 | "react/react-in-jsx-scope": "off",
18 | "react/prop-types": "off",
19 | "@typescript-eslint/no-explicit-any": "off"
20 | },
21 | "settings": {
22 | "react": {
23 | "version": "detect"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.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 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | .env
38 |
39 | /prisma/db.sqlite
40 | /prisma/db.splite-journal
41 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | strict-peer-dependencies=false
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /.vscode/cspell.json
2 |
--------------------------------------------------------------------------------
/.vscode/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2",
3 | "ignorePaths": [],
4 | "dictionaryDefinitions": [],
5 | "dictionaries": [],
6 | "words": [
7 | "codegen",
8 | "middlewares",
9 | "paralleldrive"
10 | ],
11 | "ignoreWords": [
12 | "authed",
13 | "ciphertext",
14 | "clsx",
15 | "doesn",
16 | "hasn",
17 | "kysely",
18 | "lucide",
19 | "nextauth",
20 | "nextjs",
21 | "pkce",
22 | "planetscale",
23 | "shadcn",
24 | "signin",
25 | "signout",
26 | "solidauth",
27 | "solidjs",
28 | "superjson",
29 | "tailwindcss",
30 | "tanstack",
31 | "trpc"
32 | ],
33 | "import": []
34 | }
35 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "esbenp.prettier-vscode",
4 | "dbaeumer.vscode-eslint",
5 | "bradlc.vscode-tailwindcss",
6 | "Prisma.prisma"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "javascript.validate.enable": false,
3 | "typescript.validate.enable": true,
4 | "typescript.preferences.importModuleSpecifier": "relative",
5 | "typescript.updateImportsOnFileMove.enabled": "always",
6 | "typescript.tsdk": "./node_modules/typescript/lib",
7 | "editor.defaultFormatter": "esbenp.prettier-vscode",
8 | "[prisma]": {
9 | "editor.defaultFormatter": "Prisma.prisma"
10 | },
11 | "typescript.suggest.completeFunctionCalls": true,
12 | "eslint.lintTask.enable": true,
13 | "typescript.surveys.enabled": false,
14 | "npm.autoDetect": "on",
15 | "git.inputValidationLength": 1000,
16 | "git.inputValidationSubjectLength": 100,
17 | "eslint.onIgnoredFiles": "off",
18 | "editor.tabSize": 2,
19 | "editor.detectIndentation": false,
20 | "editor.formatOnSave": true,
21 | "editor.codeActionsOnSave": {
22 | "source.organizeImports": true,
23 | "source.fixAll": true
24 | },
25 | "files.watcherExclude": {
26 | ".git": true,
27 | "node_modules": true
28 | },
29 | "typescript.enablePromptUseWorkspaceTsdk": true
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Archived
2 |
3 | This repository has been superseded by [a repo](https://github.com/mattddean/t3-app-router-edge-drizzle) that replaces Prisma and Kysely with Drizzle ORM.
4 |
5 | >It's okay to use Prisma with Kysely, but having separate schema management and querying complicates things a bit. For example, if you add the `@updatedAt` flag to a column in Prisma, Prisma relies on its runtime to update the column rather than the database, but when querying with Kysely, Kysely will not automatically provide the a value for that column. But if you use Drizzle for schema management, specifying `.onUpdateNow()` on a column will cause the database to update this column for you on each update.
6 |
7 | # T3 App Router (Edge)
8 |
9 | An experimental attempt at using the fantastic T3 Stack entirely on the Edge runtime, with Next.js's beta App Router.
10 |
11 | This is meant to be a place of hacking and learning. We're still learning how to structure apps using Next.js's new App Router, and comments are welcome in Discussions.
12 |
13 | If you encounter an error (you will), please create an Issue so that we can fix bugs and learn together.
14 |
15 | **This is not intended for production.** For a production-ready full-stack application, use much more most stable [create-t3-app](https://github.com/t3-oss/create-t3-app).
16 |
17 | This project is not affiliated with create-t3-app.
18 |
19 | ## Features
20 |
21 | This project represents the copy-pasting of work and ideas from a lot of really smart people. I think it's useful to see them all together in a working prototype.
22 |
23 | - Edge runtime for all pages and routes.
24 | - Type-safe SQL with Kysely (plus Prisma schema management)
25 | - While create-t3-app uses Prisma, Prisma can't run on the Edge runtime.
26 | - Type-safe API with tRPC
27 | - App Router setup is copied from [here](https://github.com/trpc/next-13).
28 | - The installed tRPC version is currently locked to the experimental App Router tRPC client in `./src/trpc/@trpc`, which formats the react-query query keys in a specific way that changed in later versions of tRPC. If you upgrade tRPC, hydration will stop working.
29 | - Owned Authentication with Auth.js
30 | - Kysely adapter is copied from [here](https://github.com/nextauthjs/next-auth/pull/5464).
31 | - create-t3-app uses NextAuth, which doesn't support the Edge runtime. This project uses NextAuth's successor, Auth.js, which does. Since Auth.js hasn't built support for Next.js yet, their [SolidStart implementation](https://github.com/nextauthjs/next-auth/tree/36ad964cf9aec4561dd4850c0f42b7889aa9a7db/packages/frameworks-solid-start/src) is copied and slightly modified.
32 | - Styling with [Tailwind](https://tailwindcss.com/)
33 | - It's just CSS, so it works just fine in the App Router.
34 | - React components and layout from [shadcn/ui](https://github.com/shadcn/ui)
35 | - They're also just CSS and Radix, so they work just fine in the App Router.
36 |
37 | ## Data Fetching
38 |
39 | There are a few options that Server Components + tRPC + React Query afford us. The flexibility of these tools allows us to use different strategies for different cases on the same project.
40 |
41 | 1. Fetch data on the server and render on the server or pass it to client components. [Example.](https://github.com/mattddean/t3-app-router-edge/blob/03cd3c0d16fb08a208279e08d90014e8e4fc8322/src/app/profile/page.tsx#L14)
42 | 1. Fetch data on the server and use it to hydrate react-query's cache on the client. Example: [Fetch and dehydrate data on server](https://github.com/mattddean/t3-app-router-edge/blob/c64d8dd8246491b7c4314c764b13d493b616df09/src/app/page.tsx#L19-L39), then [use cached data from server on client](https://github.com/mattddean/t3-app-router-edge/blob/03cd3c0d16fb08a208279e08d90014e8e4fc8322/src/components/posts-table.tsx#L84-L87).
43 | 1. Fetch data on the client.
44 | 1. Fetch data the server but don't block first byte and stream Server Components to the client using a Suspense boundary. TODO: Example.
45 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
65 | T3 App Router (Edge)
66 |
67 | {props.children}
68 |
( 12 | providerId?: LiteralUnion
,
13 | options?: SignInOptions,
14 | authorizationParams?: SignInAuthorizationParams,
15 | ) {
16 | const { callbackUrl = window.location.href, redirect = true } = options ?? {};
17 |
18 | // TODO: Support custom providers
19 | const isCredentials = providerId === "credentials";
20 | const isEmail = providerId === "email";
21 | const isSupportingReturn = isCredentials || isEmail;
22 |
23 | // TODO: Handle custom base path
24 | const signInUrl = `/api/auth/${isCredentials ? "callback" : "signin"}/${providerId}`;
25 |
26 | const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`;
27 |
28 | // TODO: Handle custom base path
29 | const csrfTokenResponse = await fetch("/api/auth/csrf");
30 | const { csrfToken } = await csrfTokenResponse.json();
31 |
32 | const res = await fetch(_signInUrl, {
33 | method: "post",
34 | headers: {
35 | "Content-Type": "application/x-www-form-urlencoded",
36 | "X-Auth-Return-Redirect": "1",
37 | },
38 | // @ts-expect-error -- ignore
39 | body: new URLSearchParams({
40 | ...options,
41 | csrfToken,
42 | callbackUrl,
43 | }),
44 | });
45 |
46 | const data = await res.clone().json();
47 | const error = new URL(data.url).searchParams.get("error");
48 | if (redirect || !isSupportingReturn || !error) {
49 | // TODO: Do not redirect for Credentials and Email providers by default in next major
50 | window.location.href = data.url ?? data.redirect ?? callbackUrl;
51 | // If url contains a hash, the browser does not reload the page. We reload manually
52 | if (data.url.includes("#")) window.location.reload();
53 | return;
54 | }
55 | return res;
56 | }
57 |
58 | /**
59 | * Signs the user out, by removing the session cookie.
60 | * Automatically adds the CSRF token to the request.
61 | *
62 | * [Documentation](https://next-auth.js.org/getting-started/client#signout)
63 | */
64 | export async function signOut(options?: SignOutParams) {
65 | const { callbackUrl = window.location.href } = options ?? {};
66 | // TODO: Custom base path
67 | const csrfTokenResponse = await fetch("/api/auth/csrf");
68 | const { csrfToken } = await csrfTokenResponse.json();
69 | const res = await fetch(`/api/auth/signout`, {
70 | method: "post",
71 | headers: {
72 | "Content-Type": "application/x-www-form-urlencoded",
73 | "X-Auth-Return-Redirect": "1",
74 | },
75 | body: new URLSearchParams({
76 | csrfToken,
77 | callbackUrl,
78 | }),
79 | });
80 | const data = await res.json();
81 |
82 | const url = data.url ?? data.redirect ?? callbackUrl;
83 | window.location.href = url;
84 | // If url contains a hash, the browser does not reload the page. We reload manually
85 | if (url.includes("#")) window.location.reload();
86 | }
87 |
--------------------------------------------------------------------------------
/src/auth/options.ts:
--------------------------------------------------------------------------------
1 | import GithubProvider from "@auth/core/providers/github";
2 | import GoogleProvider from "@auth/core/providers/google";
3 | import { KyselyAdapter } from "~/auth/adapters/kysely";
4 | import { db } from "~/lib/kysely-db";
5 | import { SolidAuthConfig } from "./server";
6 |
7 | export const authConfig: SolidAuthConfig = {
8 | // Configure one or more authentication providers
9 | adapter: KyselyAdapter(db),
10 | providers: [
11 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
12 | // @ts-ignore growing pains
13 | GithubProvider({
14 | clientId: process.env.GITHUB_ID as string,
15 | clientSecret: process.env.GITHUB_SECRET as string,
16 | }),
17 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
18 | // @ts-ignore growing pains
19 | GoogleProvider({
20 | clientId: process.env.GOOGLE_CLIENT_ID as string,
21 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
22 | }),
23 | ],
24 | callbacks: {
25 | session({ session, user }) {
26 | if (session.user) {
27 | session.user.id = user.id;
28 | }
29 | return session;
30 | },
31 | },
32 | session: {
33 | strategy: "database",
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/src/auth/server/index.ts:
--------------------------------------------------------------------------------
1 | /** https://github.com/nextauthjs/next-auth/blob/04791cd57478b64d0ebdfc8fe25779e2f89e2070/packages/frameworks-solid-start/src/index.ts#L1 */
2 |
3 | import { Auth } from "@auth/core";
4 | import type { AuthAction, AuthConfig, Session } from "@auth/core/types";
5 | import { serialize } from "cookie";
6 | import { parseString, splitCookiesString, type Cookie } from "set-cookie-parser";
7 |
8 | export interface SolidAuthConfig extends AuthConfig {
9 | /**
10 | * Defines the base path for the auth routes.
11 | * @default '/api/auth'
12 | */
13 | prefix?: string;
14 | }
15 |
16 | const actions: AuthAction[] = [
17 | "providers",
18 | "session",
19 | "csrf",
20 | "signin",
21 | "signout",
22 | "callback",
23 | "verify-request",
24 | "error",
25 | ];
26 |
27 | // currently multiple cookies are not supported, so we keep the next-auth.pkce.code_verifier cookie for now:
28 | // because it gets updated anyways
29 | // src: https://github.com/solidjs/solid-start/issues/293
30 | const getSetCookieCallback = (cook?: string | null): Cookie | undefined => {
31 | if (!cook) return;
32 | const splitCookie = splitCookiesString(cook);
33 | for (const cookName of [
34 | "__Secure-next-auth.session-token",
35 | "next-auth.session-token",
36 | "next-auth.pkce.code_verifier",
37 | "__Secure-next-auth.pkce.code_verifier",
38 | ]) {
39 | const temp = splitCookie.find((e) => e.startsWith(`${cookName}=`));
40 | if (temp) {
41 | return parseString(temp);
42 | }
43 | }
44 | return parseString(splitCookie?.[0] ?? ""); // just return the first cookie if no session token is found
45 | };
46 |
47 | export async function SolidAuthHandler(request: Request, prefix: string, authOptions: SolidAuthConfig) {
48 | const url = new URL(request.url);
49 | const action = url.pathname.slice(prefix.length + 1).split("/")[0] as AuthAction;
50 |
51 | if (!actions.includes(action) || !url.pathname.startsWith(prefix + "/")) {
52 | return;
53 | }
54 |
55 | const res = await Auth(request, authOptions);
56 | if (["callback", "signin", "signout"].includes(action)) {
57 | const parsedCookie = getSetCookieCallback(res.clone().headers.get("Set-Cookie"));
58 | if (parsedCookie) {
59 | res.headers.set("Set-Cookie", serialize(parsedCookie.name, parsedCookie.value, parsedCookie as any));
60 | }
61 | }
62 | return res;
63 | }
64 |
65 | export async function getSession(req: Request, options: AuthConfig): Promise {children} {siteConfig.description}
56 |
71 |
139 |
140 | {table.getHeaderGroups().map((headerGroup) => (
141 |
187 |
142 | {headerGroup.headers.map((header) => {
143 | return (
144 |
161 | ))}
162 |
163 |
164 | {table.getRowModel().rows.map((row) => {
165 | return (
166 |
150 |
158 | );
159 | })}
160 |
167 | {row.getVisibleCells().map((cell) => {
168 | return (
169 |
183 | );
184 | })}
185 |
186 |
170 |
177 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
178 |
179 |
180 | );
181 | })}
182 |