├── .nvmrc ├── apps ├── expo │ ├── index.ts │ ├── src │ │ ├── types │ │ │ └── nativewind-env.d.ts │ │ ├── utils │ │ │ ├── session-store.ts │ │ │ ├── auth.ts │ │ │ ├── base-url.tsx │ │ │ └── api.tsx │ │ ├── app │ │ │ ├── post │ │ │ │ └── [id].tsx │ │ │ ├── _layout.tsx │ │ │ └── index.tsx │ │ └── styles.css │ ├── assets │ │ └── icon.png │ ├── .expo-shared │ │ └── assets.json │ ├── turbo.json │ ├── eslint.config.mjs │ ├── babel.config.js │ ├── tailwind.config.ts │ ├── tsconfig.json │ ├── eas.json │ ├── app.config.ts │ ├── metro.config.js │ └── package.json └── nextjs │ ├── postcss.config.cjs │ ├── public │ ├── favicon.ico │ └── t3-icon.svg │ ├── src │ ├── app │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...all] │ │ │ │ │ └── route.ts │ │ │ └── trpc │ │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ ├── page.tsx │ │ ├── _components │ │ │ ├── auth-showcase.tsx │ │ │ └── posts.tsx │ │ ├── globals.css │ │ └── layout.tsx │ ├── middleware.ts │ ├── trpc │ │ ├── query-client.ts │ │ ├── server.ts │ │ └── react.tsx │ └── env.ts │ ├── turbo.json │ ├── tsconfig.json │ ├── eslint.config.js │ ├── tailwind.config.ts │ ├── next.config.js │ ├── README.md │ └── package.json ├── .npmrc ├── tooling ├── github │ ├── package.json │ └── setup │ │ └── action.yml ├── eslint │ ├── tsconfig.json │ ├── nextjs.js │ ├── react.js │ ├── package.json │ ├── types.d.ts │ └── base.js ├── prettier │ ├── tsconfig.json │ ├── package.json │ └── index.js ├── tailwind │ ├── tsconfig.json │ ├── eslint.config.js │ ├── native.ts │ ├── package.json │ ├── web.ts │ └── base.ts └── typescript │ ├── package.json │ ├── internal-package.json │ └── base.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml ├── renovate.json ├── DISCUSSION_TEMPLATE │ └── ideas.yml └── workflows │ └── ci.yml ├── packages ├── db │ ├── src │ │ ├── index.ts │ │ ├── client.ts │ │ ├── schema.ts │ │ └── auth-schema.ts │ ├── tsconfig.json │ ├── eslint.config.js │ ├── drizzle.config.ts │ └── package.json ├── auth │ ├── src │ │ ├── client.ts │ │ ├── index.ts │ │ ├── index.rsc.ts │ │ ├── middleware.ts │ │ └── auth.ts │ ├── eslint.config.js │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── env.ts │ └── package.json ├── api │ ├── tsconfig.json │ ├── eslint.config.js │ ├── src │ │ ├── root.ts │ │ ├── router │ │ │ ├── auth.ts │ │ │ └── post.ts │ │ ├── index.ts │ │ └── trpc.ts │ └── package.json ├── validators │ ├── tsconfig.json │ ├── eslint.config.js │ ├── src │ │ └── index.ts │ └── package.json └── ui │ ├── src │ ├── index.ts │ ├── label.tsx │ ├── input.tsx │ ├── toast.tsx │ ├── theme.tsx │ ├── button.tsx │ ├── form.tsx │ └── dropdown-menu.tsx │ ├── tsconfig.json │ ├── eslint.config.js │ ├── components.json │ └── package.json ├── turbo └── generators │ ├── templates │ ├── tsconfig.json.hbs │ ├── eslint.config.js.hbs │ └── package.json.hbs │ └── config.ts ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── pnpm-workspace.yaml ├── .gitignore ├── .env.example ├── LICENSE ├── package.json ├── turbo.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.16 -------------------------------------------------------------------------------- /apps/expo/index.ts: -------------------------------------------------------------------------------- 1 | import "expo-router/entry"; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | link-workspace-packages=true -------------------------------------------------------------------------------- /tooling/github/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/github" 3 | } 4 | -------------------------------------------------------------------------------- /apps/expo/src/types/nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: juliusmarminge 4 | -------------------------------------------------------------------------------- /apps/expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/create-t3-turbo/HEAD/apps/expo/assets/icon.png -------------------------------------------------------------------------------- /apps/nextjs/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/db/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "drizzle-orm/sql"; 2 | export { alias } from "drizzle-orm/pg-core"; 3 | -------------------------------------------------------------------------------- /apps/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/create-t3-turbo/HEAD/apps/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /packages/auth/src/client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from 'better-auth/react' 2 | export const authClient = createAuthClient(); -------------------------------------------------------------------------------- /tooling/eslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tooling/prettier/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tooling/tailwind/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/validators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "include": ["src"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /tooling/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/tsconfig", 3 | "private": true, 4 | "version": "0.1.0", 5 | "files": [ 6 | "*.json" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@acme/auth"; 2 | 3 | const handler = auth.handler 4 | 5 | export { handler as GET, handler as POST } 6 | -------------------------------------------------------------------------------- /apps/expo/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /turbo/generators/templates/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "compilerOptions": {}, 4 | "include": ["*.ts", "src"], 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | import { cx } from "class-variance-authority"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | const cn = (...inputs: Parameters) => twMerge(cx(inputs)); 5 | 6 | export { cn }; 7 | -------------------------------------------------------------------------------- /tooling/tailwind/eslint.config.js: -------------------------------------------------------------------------------- 1 | // FIXME: This kinda stinks... 2 | /// 3 | 4 | import baseConfig from "@acme/eslint-config/base"; 5 | 6 | export default [...baseConfig]; 7 | -------------------------------------------------------------------------------- /apps/expo/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "dev": { 6 | "persistent": true, 7 | "interactive": true 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import { auth } from "./auth"; 3 | 4 | export const getSession = async () => auth.api.getSession({ 5 | headers: headers() 6 | }); 7 | 8 | export * from "./auth"; -------------------------------------------------------------------------------- /tooling/tailwind/native.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | import base from "./base"; 4 | 5 | export default { 6 | content: base.content, 7 | presets: [base], 8 | theme: {}, 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /packages/db/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/api/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "expo.vscode-expo-tools", 5 | "esbenp.prettier-vscode", 6 | "yoavbls.pretty-ts-errors", 7 | "bradlc.vscode-tailwindcss" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/validators/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: ["dist/**"], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /turbo/generators/templates/eslint.config.js.hbs: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/auth/src/index.rsc.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | import { headers } from "next/headers"; 3 | import { auth } from "./auth"; 4 | 5 | export const getSession = async () => cache(auth.api.getSession)({ 6 | headers: headers() 7 | }); 8 | 9 | export * from "./auth"; -------------------------------------------------------------------------------- /packages/auth/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@acme/eslint-config/base"; 2 | 3 | /** @type {import('typescript-eslint').Config} */ 4 | export default [ 5 | { 6 | ignores: [], 7 | }, 8 | ...baseConfig, 9 | ...restrictEnvAccess, 10 | ]; -------------------------------------------------------------------------------- /packages/db/src/client.ts: -------------------------------------------------------------------------------- 1 | 2 | import { drizzle } from 'drizzle-orm/node-postgres'; 3 | 4 | import * as schema from "./schema"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 7 | export const db = drizzle(process.env.POSTGRES_URL!, { 8 | schema 9 | }); -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/internal-package.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "dom", "dom.iterable"], 5 | "jsx": "preserve", 6 | "rootDir": "." 7 | }, 8 | "include": ["src"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "useDefineForClassFields": true, 6 | "skipLibCheck": true 7 | }, 8 | "include": ["src", "*.ts"], 9 | "exclude": ["node_modules"], 10 | } 11 | -------------------------------------------------------------------------------- /apps/expo/src/utils/session-store.ts: -------------------------------------------------------------------------------- 1 | import * as SecureStore from "expo-secure-store"; 2 | 3 | const key = "session_token"; 4 | export const getToken = () => SecureStore.getItem(key); 5 | export const deleteToken = () => SecureStore.deleteItemAsync(key); 6 | export const setToken = (v: string) => SecureStore.setItem(key, v); 7 | -------------------------------------------------------------------------------- /apps/nextjs/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@acme/auth/middleware"; 2 | 3 | export default authMiddleware 4 | 5 | // Read more: https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher 6 | export const config = { 7 | matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], 8 | }; -------------------------------------------------------------------------------- /packages/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | import reactConfig from "@acme/eslint-config/react"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: ["dist/**"], 8 | }, 9 | ...baseConfig, 10 | ...reactConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /apps/nextjs/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": [".next/**", "!.next/cache/**", "next-env.d.ts"] 8 | }, 9 | "dev": { 10 | "persistent": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/validators/src/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const unused = z.string().describe( 4 | `This lib is currently not used as we use drizzle-zod for simple schemas 5 | But as your application grows and you need other validators to share 6 | with back and frontend, you can put them in here 7 | `, 8 | ); 9 | -------------------------------------------------------------------------------- /apps/expo/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from "@acme/eslint-config/base"; 2 | import reactConfig from "@acme/eslint-config/react"; 3 | 4 | /** @type {import('typescript-eslint').Config} */ 5 | export default [ 6 | { 7 | ignores: [".expo/**", "expo-plugins/**"], 8 | }, 9 | ...baseConfig, 10 | ...reactConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /apps/expo/babel.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("@babel/core").ConfigFunction} */ 2 | module.exports = (api) => { 3 | api.cache(true); 4 | return { 5 | presets: [ 6 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 7 | "nativewind/babel", 8 | ], 9 | plugins: ["react-native-reanimated/plugin"], 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/auth/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig((env) => { 4 | return { 5 | entry: ["src/client.ts"], 6 | format: ["esm", "cjs"], 7 | bundle: true, 8 | splitting: false, 9 | noExternal: ["better-auth"], 10 | dts: true, 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /apps/expo/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | // @ts-expect-error - no types 3 | import nativewind from "nativewind/preset"; 4 | 5 | import baseConfig from "@acme/tailwind-config/native"; 6 | 7 | export default { 8 | content: ["./src/**/*.{ts,tsx}"], 9 | presets: [baseConfig, nativewind], 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /packages/api/src/root.ts: -------------------------------------------------------------------------------- 1 | import { authRouter } from "./router/auth"; 2 | import { postRouter } from "./router/post"; 3 | import { createTRPCRouter } from "./trpc"; 4 | 5 | export const appRouter = createTRPCRouter({ 6 | auth: authRouter, 7 | post: postRouter, 8 | }); 9 | 10 | // export type definition of API 11 | export type AppRouter = typeof appRouter; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask a question 3 | url: https://github.com/t3-oss/create-t3-turbo/discussions 4 | about: Ask questions and discuss with other community members 5 | - name: Feature request 6 | url: https://github.com/t3-oss/create-t3-turbo/discussions/new?category=ideas 7 | about: Feature requests should be opened as discussions 8 | -------------------------------------------------------------------------------- /tooling/typescript/internal-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | /** Emit types for internal packages to speed up editor performance. */ 6 | "declaration": true, 7 | "declarationMap": true, 8 | "emitDeclarationOnly": true, 9 | "noEmit": false, 10 | "outDir": "${configDir}/dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "lib": ["ES2022", "dom", "dom.iterable"], 5 | "jsx": "preserve", 6 | "baseUrl": ".", 7 | "paths": { 8 | "~/*": ["./src/*"] 9 | }, 10 | "plugins": [{ "name": "next" }], 11 | "module": "esnext" 12 | }, 13 | "include": [".", ".next/types/**/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/db/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | if (!process.env.POSTGRES_URL) { 4 | throw new Error("Missing POSTGRES_URL"); 5 | } 6 | 7 | const nonPoolingUrl = process.env.POSTGRES_URL.replace(":6543", ":5432"); 8 | 9 | export default { 10 | schema: "./src/schema.ts", 11 | dialect: "postgresql", 12 | dbCredentials: { url: nonPoolingUrl }, 13 | casing: "snake_case", 14 | } satisfies Config; -------------------------------------------------------------------------------- /packages/api/src/router/auth.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | import { protectedProcedure, publicProcedure } from "../trpc"; 3 | 4 | export const authRouter = { 5 | getSession: publicProcedure.query(({ ctx }) => { 6 | return ctx.session; 7 | }), 8 | getSecretMessage: protectedProcedure.query(() => { 9 | return "you can see this secret message!"; 10 | }), 11 | } satisfies TRPCRouterRecord; 12 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "../../tooling/tailwind/web.ts", 8 | "css": "unused.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "utils": "@acme/ui", 14 | "components": "src/", 15 | "ui": "src/" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tooling/github/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup and install" 2 | description: "Common setup steps for Actions" 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: pnpm/action-setup@v4 8 | - uses: actions/setup-node@v4 9 | with: 10 | node-version-file: ".nvmrc" 11 | cache: "pnpm" 12 | 13 | - shell: bash 14 | run: pnpm add -g turbo 15 | 16 | - shell: bash 17 | run: pnpm install 18 | -------------------------------------------------------------------------------- /apps/nextjs/eslint.config.js: -------------------------------------------------------------------------------- 1 | import baseConfig, { restrictEnvAccess } from "@acme/eslint-config/base"; 2 | import nextjsConfig from "@acme/eslint-config/nextjs"; 3 | import reactConfig from "@acme/eslint-config/react"; 4 | 5 | /** @type {import('typescript-eslint').Config} */ 6 | export default [ 7 | { 8 | ignores: [".next/**"], 9 | }, 10 | ...baseConfig, 11 | ...reactConfig, 12 | ...nextjsConfig, 13 | ...restrictEnvAccess, 14 | ]; 15 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "matchPackagePatterns": ["^@acme/"], 7 | "enabled": false 8 | } 9 | ], 10 | "updateInternalDeps": true, 11 | "rangeStrategy": "bump", 12 | "automerge": true, 13 | "npm": { 14 | "fileMatch": ["(^|/)package\\.json$", "(^|/)package\\.json\\.hbs$"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/expo/src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from 'better-auth/react' 2 | import { expoClient } from '@better-auth/expo/client' 3 | import * as SecureStore from "expo-secure-store" 4 | 5 | 6 | export const authClient = createAuthClient({ 7 | plugins: [expoClient({ 8 | scheme: "expo", 9 | storage: SecureStore 10 | })], 11 | baseURL: "http://localhost:3000", 12 | }) 13 | 14 | export const { signIn, signOut } = authClient 15 | 16 | -------------------------------------------------------------------------------- /apps/expo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@acme/tsconfig/base.json"], 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "~/*": ["./src/*"] 7 | }, 8 | "jsx": "react-native", 9 | "types": ["nativewind/types"], 10 | "checkJs": false, 11 | "moduleSuffixes": [".ios", ".android", ".native", ""] 12 | }, 13 | "include": ["src", "*.ts", "*.js", ".expo/types/**/*.ts", "expo-env.d.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "pnpm dev", 9 | "cwd": "${workspaceFolder}/apps/nextjs", 10 | "skipFiles": ["/**"], 11 | "sourceMaps": true, 12 | "sourceMapPathOverrides": { 13 | "/turbopack/[project]/*": "${webRoot}/*" //https://github.com/vercel/next.js/issues/62008 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tooling/eslint/nextjs.js: -------------------------------------------------------------------------------- 1 | import nextPlugin from "@next/eslint-plugin-next"; 2 | 3 | /** @type {Awaited} */ 4 | export default [ 5 | { 6 | files: ["**/*.ts", "**/*.tsx"], 7 | plugins: { 8 | "@next/next": nextPlugin, 9 | }, 10 | rules: { 11 | ...nextPlugin.configs.recommended.rules, 12 | ...nextPlugin.configs["core-web-vitals"].rules, 13 | // TypeError: context.getAncestors is not a function 14 | "@next/next/no-duplicate-head": "off", 15 | }, 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - packages/* 4 | - tooling/* 5 | 6 | catalog: 7 | "@tanstack/react-query": ^5.59.15 8 | "@trpc/client": ^11.0.0-rc.477 9 | "@trpc/react-query": ^11.0.0-rc.477 10 | "@trpc/server": ^11.0.0-rc.477 11 | eslint: ^9.12.0 12 | prettier: ^3.3.3 13 | tailwindcss: ^3.4.14 14 | typescript: ^5.6.3 15 | zod: ^3.23.8 16 | better-auth: ^0.7.3 17 | 18 | catalogs: 19 | react18: 20 | react: 18.3.1 21 | react-dom: 18.3.1 22 | "@types/react": ^18.3.11 23 | "@types/react-dom": ^18.3.1 24 | -------------------------------------------------------------------------------- /tooling/eslint/react.js: -------------------------------------------------------------------------------- 1 | import reactPlugin from "eslint-plugin-react"; 2 | import hooksPlugin from "eslint-plugin-react-hooks"; 3 | 4 | /** @type {Awaited} */ 5 | export default [ 6 | { 7 | files: ["**/*.ts", "**/*.tsx"], 8 | plugins: { 9 | react: reactPlugin, 10 | "react-hooks": hooksPlugin, 11 | }, 12 | rules: { 13 | ...reactPlugin.configs["jsx-runtime"].rules, 14 | ...hooksPlugin.configs.recommended.rules, 15 | }, 16 | languageOptions: { 17 | globals: { 18 | React: "writable", 19 | }, 20 | }, 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /packages/auth/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | AUTH_DISCORD_ID: z.string().min(1), 7 | BETTER_AUTH_URL: z.string().min(1), 8 | AUTH_DISCORD_SECRET: z.string().min(1), 9 | AUTH_SECRET: 10 | process.env.NODE_ENV === "production" 11 | ? z.string().min(1) 12 | : z.string().min(1).optional(), 13 | NODE_ENV: z.enum(["development", "production"]).optional(), 14 | }, 15 | client: {}, 16 | experimental__runtimeEnv: {}, 17 | skipValidation: 18 | !!process.env.CI || process.env.npm_lifecycle_event === "lint", 19 | }); 20 | -------------------------------------------------------------------------------- /apps/expo/eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 4.1.2" 4 | }, 5 | "build": { 6 | "base": { 7 | "node": "20.16.0", 8 | "pnpm": "9.7.0", 9 | "ios": { 10 | "resourceClass": "m-medium" 11 | } 12 | }, 13 | "development": { 14 | "extends": "base", 15 | "developmentClient": true, 16 | "distribution": "internal" 17 | }, 18 | "preview": { 19 | "extends": "base", 20 | "distribution": "internal", 21 | "ios": { 22 | "simulator": true 23 | } 24 | }, 25 | "production": { 26 | "extends": "base" 27 | } 28 | }, 29 | "submit": { 30 | "production": {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/nextjs/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { fontFamily } from "tailwindcss/defaultTheme"; 3 | 4 | import baseConfig from "@acme/tailwind-config/web"; 5 | 6 | export default { 7 | // We need to append the path to the UI package to the content array so that 8 | // those classes are included correctly. 9 | content: [...baseConfig.content, "../../packages/ui/src/*.{ts,tsx}"], 10 | presets: [baseConfig], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["var(--font-geist-sans)", ...fontFamily.sans], 15 | mono: ["var(--font-geist-mono)", ...fontFamily.mono], 16 | }, 17 | }, 18 | }, 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /tooling/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/prettier-config", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./index.js" 8 | }, 9 | "scripts": { 10 | "clean": "git clean -xdf .cache .turbo node_modules", 11 | "format": "prettier --check . --ignore-path ../../.gitignore", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@ianvs/prettier-plugin-sort-imports": "^4.3.1", 16 | "prettier": "catalog:", 17 | "prettier-plugin-tailwindcss": "^0.6.8" 18 | }, 19 | "devDependencies": { 20 | "@acme/tsconfig": "workspace:*", 21 | "typescript": "catalog:" 22 | }, 23 | "prettier": "@acme/prettier-config" 24 | } 25 | -------------------------------------------------------------------------------- /.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 | next-env.d.ts 15 | 16 | # nitro 17 | .nitro/ 18 | .output/ 19 | 20 | # expo 21 | .expo/ 22 | expo-env.d.ts 23 | apps/expo/.gitignore 24 | apps/expo/ios 25 | apps/expo/android 26 | 27 | # production 28 | build 29 | 30 | # misc 31 | .DS_Store 32 | *.pem 33 | 34 | # debug 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | .pnpm-debug.log* 39 | 40 | # local env files 41 | .env 42 | .env*.local 43 | 44 | # vercel 45 | .vercel 46 | 47 | # typescript 48 | dist/ 49 | .cache 50 | 51 | # turbo 52 | .turbo 53 | -------------------------------------------------------------------------------- /apps/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | import createJiti from "jiti"; 3 | 4 | // Import env files to validate at build time. Use jiti so we can load .ts files in here. 5 | createJiti(fileURLToPath(import.meta.url))("./src/env"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | 11 | /** Enables hot reloading for local packages without a build step */ 12 | transpilePackages: [ 13 | "@acme/api", 14 | "@acme/auth", 15 | "@acme/db", 16 | "@acme/ui", 17 | "@acme/validators", 18 | ], 19 | 20 | /** We already do linting and typechecking as separate tasks in CI */ 21 | eslint: { ignoreDuringBuilds: true }, 22 | typescript: { ignoreBuildErrors: true }, 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/query-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultShouldDehydrateQuery, 3 | QueryClient, 4 | } from "@tanstack/react-query"; 5 | import SuperJSON from "superjson"; 6 | 7 | export const createQueryClient = () => 8 | new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | // With SSR, we usually want to set some default staleTime 12 | // above 0 to avoid refetching immediately on the client 13 | staleTime: 30 * 1000, 14 | }, 15 | dehydrate: { 16 | serializeData: SuperJSON.serialize, 17 | shouldDehydrateQuery: (query) => 18 | defaultShouldDehydrateQuery(query) || 19 | query.state.status === "pending", 20 | }, 21 | hydrate: { 22 | deserializeData: SuperJSON.deserialize, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /packages/db/src/schema.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm"; 2 | import { pgTable } from "drizzle-orm/pg-core"; 3 | import { createInsertSchema } from "drizzle-zod"; 4 | import { z } from "zod"; 5 | 6 | export const Post = pgTable("post", (t) => ({ 7 | id: t.uuid().notNull().primaryKey().defaultRandom(), 8 | title: t.varchar({ length: 256 }).notNull(), 9 | content: t.text().notNull(), 10 | createdAt: t.timestamp().defaultNow().notNull(), 11 | updatedAt: t 12 | .timestamp({ mode: "date", withTimezone: true }) 13 | .$onUpdateFn(() => sql`now()`), 14 | })); 15 | 16 | export const CreatePostSchema = createInsertSchema(Post, { 17 | title: z.string().max(256), 18 | content: z.string().max(256), 19 | }).omit({ 20 | id: true, 21 | createdAt: true, 22 | updatedAt: true, 23 | }); 24 | 25 | export * from "./auth-schema" -------------------------------------------------------------------------------- /turbo/generators/templates/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/{{ name }}", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./src/index.ts" 8 | }, 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "tsc", 12 | "clean": "git clean -xdf .cache .turbo dist node_modules", 13 | "dev": "tsc", 14 | "format": "prettier --check . --ignore-path ../../.gitignore", 15 | "lint": "eslint", 16 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 17 | }, 18 | "devDependencies": { 19 | "@acme/eslint-config": "workspace:*", 20 | "@acme/prettier-config": "workspace:*", 21 | "@acme/tsconfig": "workspace:*", 22 | "eslint": "catalog:", 23 | "prettier": "catalog:", 24 | "typescript": "catalog:" 25 | }, 26 | "prettier": "@acme/prettier-config" 27 | } 28 | -------------------------------------------------------------------------------- /apps/expo/src/app/post/[id].tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView, Text, View } from "react-native"; 2 | import { Stack, useGlobalSearchParams } from "expo-router"; 3 | 4 | import { api } from "~/utils/api"; 5 | 6 | export default function Post() { 7 | const { id } = useGlobalSearchParams(); 8 | if (!id || typeof id !== "string") throw new Error("unreachable"); 9 | const { data } = api.post.byId.useQuery({ id }); 10 | 11 | if (!data) return null; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | {data.title} 19 | 20 | {data.content} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/src/label.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@acme/ui"; 7 | 8 | const labelVariants = cva( 9 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 10 | ); 11 | 12 | const Label = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef & 15 | VariantProps 16 | >(({ className, ...props }, ref) => ( 17 | 22 | )); 23 | Label.displayName = LabelPrimitive.Root.displayName; 24 | 25 | export { Label }; 26 | -------------------------------------------------------------------------------- /packages/auth/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from 'better-auth/client' 2 | import type { NextRequest } from 'next/server'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export const client = createAuthClient(); 6 | 7 | export async function authMiddleware(request: NextRequest) { 8 | /** 9 | * This is an example of how you can use the client to get the session 10 | * from the request headers. 11 | * 12 | * You can then use this session to make decisions about the request 13 | */ 14 | const { data: session } = await client.getSession({ 15 | fetchOptions: { 16 | headers: { 17 | cookie: request.headers.get('cookie') ?? "" 18 | } 19 | } 20 | }) 21 | if (!session) { 22 | NextResponse.redirect(new URL("/sign-in", request.url)); 23 | } 24 | return NextResponse.next(); 25 | } -------------------------------------------------------------------------------- /packages/ui/src/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@acme/ui"; 4 | 5 | type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /tooling/tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/tailwind-config", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | "./native": "./native.ts", 8 | "./web": "./web.ts" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "clean": "git clean -xdf .cache .turbo node_modules", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "lint": "eslint", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "postcss": "^8.4.47", 19 | "tailwindcss": "catalog:", 20 | "tailwindcss-animate": "^1.0.7" 21 | }, 22 | "devDependencies": { 23 | "@acme/eslint-config": "workspace:*", 24 | "@acme/prettier-config": "workspace:*", 25 | "@acme/tsconfig": "workspace:*", 26 | "eslint": "catalog:", 27 | "prettier": "catalog:", 28 | "typescript": "catalog:" 29 | }, 30 | "prettier": "@acme/prettier-config" 31 | } 32 | -------------------------------------------------------------------------------- /tooling/typescript/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | /** Base Options */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "ES2022", 8 | "lib": ["ES2022"], 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | "moduleDetection": "force", 12 | "isolatedModules": true, 13 | 14 | /** Keep TSC performant in monorepos */ 15 | "incremental": true, 16 | "disableSourceOfProjectReferenceRedirect": true, 17 | "tsBuildInfoFile": "${configDir}/.cache/tsbuildinfo.json", 18 | 19 | /** Strictness */ 20 | "strict": true, 21 | "noUncheckedIndexedAccess": true, 22 | "checkJs": true, 23 | 24 | /** Transpile using Bundler (not tsc) */ 25 | "module": "Preserve", 26 | "moduleResolution": "Bundler", 27 | "noEmit": true 28 | }, 29 | "exclude": ["node_modules", "build", "dist", ".next", ".expo"] 30 | } 31 | -------------------------------------------------------------------------------- /packages/auth/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@acme/db/client"; 2 | import { oAuthProxy } from "better-auth/plugins" 3 | import type { BetterAuthOptions } from "better-auth"; 4 | import { betterAuth } from "better-auth"; 5 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 6 | import { env } from "../env"; 7 | import { expo } from "@better-auth/expo"; 8 | 9 | export const config = { 10 | database: drizzleAdapter(db, { 11 | provider: "pg" 12 | }), 13 | secret: env.AUTH_SECRET, 14 | plugins: [oAuthProxy(), expo()], 15 | socialProviders: { 16 | discord: { 17 | clientId: env.AUTH_DISCORD_ID, 18 | clientSecret: env.AUTH_DISCORD_SECRET, 19 | redirectURI: "http://localhost:3000/api/auth/callback/discord", 20 | } 21 | }, 22 | trustedOrigins: ["exp://"] 23 | } satisfies BetterAuthOptions 24 | 25 | export const auth = betterAuth(config); 26 | export type Session = typeof auth.$Infer.Session 27 | -------------------------------------------------------------------------------- /packages/validators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/validators", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | } 11 | }, 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "tsc", 15 | "clean": "git clean -xdf .cache .turbo dist node_modules", 16 | "dev": "tsc", 17 | "format": "prettier --check . --ignore-path ../../.gitignore", 18 | "lint": "eslint", 19 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 20 | }, 21 | "dependencies": { 22 | "zod": "catalog:" 23 | }, 24 | "devDependencies": { 25 | "@acme/eslint-config": "workspace:*", 26 | "@acme/prettier-config": "workspace:*", 27 | "@acme/tsconfig": "workspace:*", 28 | "eslint": "catalog:", 29 | "prettier": "catalog:", 30 | "typescript": "catalog:" 31 | }, 32 | "prettier": "@acme/prettier-config" 33 | } 34 | -------------------------------------------------------------------------------- /apps/nextjs/public/t3-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import { cache } from "react"; 2 | import { headers } from "next/headers"; 3 | import { createHydrationHelpers } from "@trpc/react-query/rsc"; 4 | 5 | import type { AppRouter } from "@acme/api"; 6 | import { createCaller, createTRPCContext } from "@acme/api"; 7 | 8 | import { createQueryClient } from "./query-client"; 9 | 10 | /** 11 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when 12 | * handling a tRPC call from a React Server Component. 13 | */ 14 | const createContext = cache(async () => { 15 | const heads = new Headers(headers()); 16 | heads.set("x-trpc-source", "rsc"); 17 | 18 | return createTRPCContext({ 19 | headers: heads, 20 | }); 21 | }); 22 | 23 | const getQueryClient = cache(createQueryClient); 24 | const caller = createCaller(createContext); 25 | 26 | export const { trpc: api, HydrateClient } = createHydrationHelpers( 27 | caller, 28 | getQueryClient, 29 | ); 30 | -------------------------------------------------------------------------------- /apps/expo/src/utils/base-url.tsx: -------------------------------------------------------------------------------- 1 | import Constants from "expo-constants"; 2 | 3 | /** 4 | * Extend this function when going to production by 5 | * setting the baseUrl to your production API URL. 6 | */ 7 | export const getBaseUrl = () => { 8 | /** 9 | * Gets the IP address of your host-machine. If it cannot automatically find it, 10 | * you'll have to manually set it. NOTE: Port 3000 should work for most but confirm 11 | * you don't have anything else running on it, or you'd have to change it. 12 | * 13 | * **NOTE**: This is only for development. In production, you'll want to set the 14 | * baseUrl to your production API URL. 15 | */ 16 | const debuggerHost = Constants.expoConfig?.hostUri; 17 | const localhost = debuggerHost?.split(":")[0]; 18 | 19 | if (!localhost) { 20 | // return "https://turbo.t3.gg"; 21 | throw new Error( 22 | "Failed to get localhost. Please point to your production server.", 23 | ); 24 | } 25 | return `http://${localhost}:3000`; 26 | }; 27 | -------------------------------------------------------------------------------- /.github/DISCUSSION_TEMPLATE/ideas.yml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: markdown 3 | attributes: 4 | value: | 5 | Thank you for taking the time to file a feature request. Please fill out this form as completely as possible. 6 | - type: textarea 7 | attributes: 8 | label: Describe the feature you'd like to request 9 | description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: Describe the solution you'd like to see 15 | description: Please describe the solution you would like to see. Adding example usage is a good way to provide context. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Additional information 21 | description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. 22 | -------------------------------------------------------------------------------- /apps/expo/src/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import "@bacons/text-decoder/install"; 2 | 3 | import { Stack } from "expo-router"; 4 | import { StatusBar } from "expo-status-bar"; 5 | import { useColorScheme } from "nativewind"; 6 | 7 | import { TRPCProvider } from "~/utils/api"; 8 | 9 | import "../styles.css"; 10 | 11 | // This is the main layout of the app 12 | // It wraps your pages with the providers they need 13 | export default function RootLayout() { 14 | const { colorScheme } = useColorScheme(); 15 | return ( 16 | 17 | {/* 18 | The Stack component displays the current page. 19 | It also allows you to configure your screens 20 | */} 21 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /.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 | # The database URL is used to connect to your Supabase database. 8 | POSTGRES_URL="postgres://postgres.[USERNAME]:[PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?workaround=supabase-pooler.vercel" 9 | 10 | 11 | # You can generate the secret via 'openssl rand -base64 32' on Unix 12 | # @see https://next-auth.js.org/configuration/options#secret 13 | AUTH_SECRET='supersecret' 14 | 15 | # Preconfigured Discord OAuth provider, works out-of-the-box 16 | # @see https://next-auth.js.org/providers/discord 17 | AUTH_DISCORD_ID='' 18 | AUTH_DISCORD_SECRET='' 19 | 20 | # The URL of your next js app 21 | BETTER_AUTH_URL='http://localhost:3000' -------------------------------------------------------------------------------- /packages/ui/src/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, toast } from "sonner"; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster, toast }; 32 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | 3 | import type { AppRouter } from "./root"; 4 | import { appRouter } from "./root"; 5 | import { createCallerFactory, createTRPCContext } from "./trpc"; 6 | 7 | /** 8 | * Create a server-side caller for the tRPC API 9 | * @example 10 | * const trpc = createCaller(createContext); 11 | * const res = await trpc.post.all(); 12 | * ^? Post[] 13 | */ 14 | const createCaller = createCallerFactory(appRouter); 15 | 16 | /** 17 | * Inference helpers for input types 18 | * @example 19 | * type PostByIdInput = RouterInputs['post']['byId'] 20 | * ^? { id: number } 21 | **/ 22 | type RouterInputs = inferRouterInputs; 23 | 24 | /** 25 | * Inference helpers for output types 26 | * @example 27 | * type AllPostsOutput = RouterOutputs['post']['all'] 28 | * ^? Post[] 29 | **/ 30 | type RouterOutputs = inferRouterOutputs; 31 | 32 | export { createTRPCContext, appRouter, createCaller }; 33 | export type { AppRouter, RouterInputs, RouterOutputs }; 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "[typescript,typescriptreact]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "editor.formatOnSave": true, 10 | "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], 11 | "eslint.runtime": "node", 12 | "eslint.workingDirectories": [ 13 | { "pattern": "apps/*/" }, 14 | { "pattern": "packages/*/" }, 15 | { "pattern": "tooling/*/" } 16 | ], 17 | "prettier.ignorePath": ".gitignore", 18 | "tailwindCSS.experimental.classRegex": [ 19 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 20 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 21 | ], 22 | "tailwindCSS.experimental.configFile": "./tooling/tailwind/web.ts", 23 | "typescript.enablePromptUseWorkspaceTsdk": true, 24 | "typescript.preferences.autoImportFileExcludePatterns": [ 25 | "next/router.d.ts", 26 | "next/dist/client/router.d.ts" 27 | ], 28 | "typescript.tsdk": "node_modules/typescript/lib" 29 | } 30 | -------------------------------------------------------------------------------- /apps/expo/app.config.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigContext, ExpoConfig } from "expo/config"; 2 | 3 | export default ({ config }: ConfigContext): ExpoConfig => ({ 4 | ...config, 5 | name: "expo", 6 | slug: "expo", 7 | scheme: "expo", 8 | version: "0.1.0", 9 | orientation: "portrait", 10 | icon: "./assets/icon.png", 11 | userInterfaceStyle: "automatic", 12 | splash: { 13 | image: "./assets/icon.png", 14 | resizeMode: "contain", 15 | backgroundColor: "#1F104A" 16 | }, 17 | updates: { 18 | fallbackToCacheTimeout: 0, 19 | }, 20 | assetBundlePatterns: ["**/*"], 21 | ios: { 22 | bundleIdentifier: "your.bundle.identifier", 23 | supportsTablet: true, 24 | }, 25 | android: { 26 | package: "your.bundle.identifier", 27 | adaptiveIcon: { 28 | foregroundImage: "./assets/icon.png", 29 | backgroundColor: "#1F104A", 30 | }, 31 | }, 32 | // extra: { 33 | // eas: { 34 | // projectId: "your-eas-project-id", 35 | // }, 36 | // }, 37 | experiments: { 38 | tsconfigPaths: true, 39 | typedRoutes: true, 40 | }, 41 | plugins: ["expo-router", "expo-secure-store"], 42 | }); 43 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/api", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | } 11 | }, 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "tsc", 15 | "clean": "git clean -xdf .cache .turbo dist node_modules", 16 | "dev": "tsc", 17 | "format": "prettier --check . --ignore-path ../../.gitignore", 18 | "lint": "eslint", 19 | "typecheck": "tsc --noEmit --emitDeclarationOnly false" 20 | }, 21 | "dependencies": { 22 | "@acme/auth": "workspace:*", 23 | "@acme/db": "workspace:*", 24 | "@acme/validators": "workspace:*", 25 | "@trpc/server": "catalog:", 26 | "superjson": "2.2.1", 27 | "zod": "catalog:" 28 | }, 29 | "devDependencies": { 30 | "@acme/eslint-config": "workspace:*", 31 | "@acme/prettier-config": "workspace:*", 32 | "@acme/tsconfig": "workspace:*", 33 | "eslint": "catalog:", 34 | "prettier": "catalog:", 35 | "typescript": "catalog:" 36 | }, 37 | "prettier": "@acme/prettier-config" 38 | } 39 | -------------------------------------------------------------------------------- /tooling/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/eslint-config", 3 | "private": true, 4 | "version": "0.3.0", 5 | "type": "module", 6 | "exports": { 7 | "./base": "./base.js", 8 | "./nextjs": "./nextjs.js", 9 | "./react": "./react.js" 10 | }, 11 | "scripts": { 12 | "clean": "git clean -xdf .cache .turbo node_modules", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "typecheck": "tsc --noEmit" 15 | }, 16 | "dependencies": { 17 | "@eslint/compat": "^1.2.0", 18 | "@next/eslint-plugin-next": "^14.2.15", 19 | "eslint-plugin-import": "^2.31.0", 20 | "eslint-plugin-jsx-a11y": "^6.10.0", 21 | "eslint-plugin-react": "^7.37.1", 22 | "eslint-plugin-react-hooks": "^5.0.0", 23 | "eslint-plugin-turbo": "^2.1.3", 24 | "typescript-eslint": "^8.9.0" 25 | }, 26 | "devDependencies": { 27 | "@acme/prettier-config": "workspace:*", 28 | "@acme/tsconfig": "workspace:*", 29 | "@types/eslint__js": "8.42.3", 30 | "eslint": "catalog:", 31 | "prettier": "catalog:", 32 | "typescript": "catalog:" 33 | }, 34 | "prettier": "@acme/prettier-config" 35 | } 36 | -------------------------------------------------------------------------------- /tooling/tailwind/web.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import animate from "tailwindcss-animate"; 3 | 4 | import base from "./base"; 5 | 6 | export default { 7 | content: base.content, 8 | presets: [base], 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: "2rem", 13 | screens: { 14 | "2xl": "1400px", 15 | }, 16 | }, 17 | extend: { 18 | borderRadius: { 19 | lg: "var(--radius)", 20 | md: "calc(var(--radius) - 2px)", 21 | sm: "calc(var(--radius) - 4px)", 22 | }, 23 | keyframes: { 24 | "accordion-down": { 25 | from: { height: "0" }, 26 | to: { height: "var(--radix-accordion-content-height)" }, 27 | }, 28 | "accordion-up": { 29 | from: { height: "var(--radix-accordion-content-height)" }, 30 | to: { height: "0" }, 31 | }, 32 | }, 33 | animation: { 34 | "accordion-down": "accordion-down 0.2s ease-out", 35 | "accordion-up": "accordion-up 0.2s ease-out", 36 | }, 37 | }, 38 | }, 39 | plugins: [animate], 40 | } satisfies Config; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Julius Marminge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/nextjs/src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { vercel } from "@t3-oss/env-nextjs/presets"; 3 | import { z } from "zod"; 4 | 5 | import { env as authEnv } from "@acme/auth/env"; 6 | 7 | export const env = createEnv({ 8 | extends: [authEnv, vercel()], 9 | shared: { 10 | NODE_ENV: z 11 | .enum(["development", "production", "test"]) 12 | .default("development"), 13 | }, 14 | /** 15 | * Specify your server-side environment variables schema here. 16 | * This way you can ensure the app isn't built with invalid env vars. 17 | */ 18 | server: { 19 | POSTGRES_URL: z.string().url(), 20 | }, 21 | 22 | /** 23 | * Specify your client-side environment variables schema here. 24 | * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`. 25 | */ 26 | client: { 27 | // NEXT_PUBLIC_CLIENTVAR: z.string(), 28 | }, 29 | /** 30 | * Destructure all variables from `process.env` to make sure they aren't tree-shaken away. 31 | */ 32 | experimental__runtimeEnv: { 33 | NODE_ENV: process.env.NODE_ENV, 34 | 35 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 36 | }, 37 | skipValidation: 38 | !!process.env.CI || process.env.npm_lifecycle_event === "lint", 39 | }); 40 | -------------------------------------------------------------------------------- /packages/api/src/router/post.ts: -------------------------------------------------------------------------------- 1 | import type { TRPCRouterRecord } from "@trpc/server"; 2 | import { z } from "zod"; 3 | 4 | import { desc, eq } from "@acme/db"; 5 | import { CreatePostSchema, Post } from "@acme/db/schema"; 6 | 7 | import { protectedProcedure, publicProcedure } from "../trpc"; 8 | 9 | export const postRouter = { 10 | all: publicProcedure.query(({ ctx }) => { 11 | // return ctx.db.select().from(schema.post).orderBy(desc(schema.post.id)); 12 | return ctx.db.query.Post.findMany({ 13 | orderBy: desc(Post.id), 14 | limit: 10, 15 | }); 16 | }), 17 | 18 | byId: publicProcedure 19 | .input(z.object({ id: z.string() })) 20 | .query(({ ctx, input }) => { 21 | // return ctx.db 22 | // .select() 23 | // .from(schema.post) 24 | // .where(eq(schema.post.id, input.id)); 25 | 26 | return ctx.db.query.Post.findFirst({ 27 | where: eq(Post.id, input.id), 28 | }); 29 | }), 30 | 31 | create: protectedProcedure 32 | .input(CreatePostSchema) 33 | .mutation(({ ctx, input }) => { 34 | return ctx.db.insert(Post).values(input); 35 | }), 36 | 37 | delete: protectedProcedure.input(z.string()).mutation(({ ctx, input }) => { 38 | return ctx.db.delete(Post).where(eq(Post.id, input)); 39 | }), 40 | } satisfies TRPCRouterRecord; 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Create a bug report to help us improve 3 | title: "bug: " 4 | labels: ["🐞❔ unconfirmed bug"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Provide environment information 9 | description: | 10 | Run this command in your project root and paste the results in a code block: 11 | ```bash 12 | npx envinfo --system --binaries 13 | ``` 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Describe the bug 19 | description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. 20 | validations: 21 | required: true 22 | - type: input 23 | attributes: 24 | label: Link to reproduction 25 | description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored. 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: To reproduce 31 | description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Additional information 37 | description: Add any other information related to the bug here, screenshots if applicable. 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-t3-turbo", 3 | "private": true, 4 | "engines": { 5 | "node": ">=20.16.0", 6 | "pnpm": "^9.6.0" 7 | }, 8 | "packageManager": "pnpm@9.12.1", 9 | "scripts": { 10 | "build": "turbo run build", 11 | "clean": "git clean -xdf node_modules", 12 | "clean:workspaces": "turbo run clean", 13 | "db:push": "turbo -F @acme/db push", 14 | "auth:generate": "pnpm -F @acme/auth generate", 15 | "db:studio": "turbo -F @acme/db studio", 16 | "dev": "turbo watch dev --continue", 17 | "dev:next": "turbo watch dev -F @acme/nextjs...", 18 | "format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache", 19 | "format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache", 20 | "lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache", 21 | "lint:fix": "turbo run lint --continue -- --fix --cache --cache-location .cache/.eslintcache", 22 | "lint:ws": "pnpm dlx sherif@latest", 23 | "postinstall": "pnpm lint:ws", 24 | "typecheck": "turbo run typecheck", 25 | "ui-add": "turbo run ui-add" 26 | }, 27 | "devDependencies": { 28 | "@acme/prettier-config": "workspace:*", 29 | "@turbo/gen": "^2.1.3", 30 | "prettier": "catalog:", 31 | "turbo": "^2.1.3", 32 | "typescript": "catalog:" 33 | }, 34 | "prettier": "@acme/prettier-config" 35 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: ["*"] 6 | push: 7 | branches: ["main"] 8 | merge_group: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 13 | 14 | # You can leverage Vercel Remote Caching with Turbo to speed up your builds 15 | # @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds 16 | env: 17 | FORCE_COLOR: 3 18 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 19 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 20 | 21 | jobs: 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Setup 28 | uses: ./tooling/github/setup 29 | 30 | - name: Copy env 31 | shell: bash 32 | run: cp .env.example .env 33 | 34 | - name: Lint 35 | run: pnpm lint && pnpm lint:ws 36 | 37 | format: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Setup 43 | uses: ./tooling/github/setup 44 | 45 | - name: Format 46 | run: pnpm format 47 | 48 | typecheck: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | 53 | - name: Setup 54 | uses: ./tooling/github/setup 55 | 56 | - name: Typecheck 57 | run: pnpm typecheck 58 | -------------------------------------------------------------------------------- /packages/ui/src/theme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; 5 | import { ThemeProvider, useTheme } from "next-themes"; 6 | 7 | import { Button } from "./button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "./dropdown-menu"; 14 | 15 | function ThemeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | } 41 | 42 | export { ThemeProvider, ThemeToggle }; 43 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | 3 | import { api, HydrateClient } from "~/trpc/server"; 4 | import { AuthShowcase } from "./_components/auth-showcase"; 5 | import { 6 | CreatePostForm, 7 | PostCardSkeleton, 8 | PostList, 9 | } from "./_components/posts"; 10 | 11 | export const runtime = "edge"; 12 | 13 | export default function HomePage() { 14 | // You can await this here if you don't want to show Suspense fallback below 15 | void api.post.all.prefetch(); 16 | 17 | return ( 18 | 19 |
20 |
21 |

22 | Create T3 Turbo 23 |

24 | 25 | 26 | 27 |
28 | 31 | 32 | 33 | 34 |
35 | } 36 | > 37 | 38 | 39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /apps/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Drizzle](https://orm.drizzle.team) 14 | - [Tailwind CSS](https://tailwindcss.com) 15 | - [tRPC](https://trpc.io) 16 | 17 | ## Learn More 18 | 19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 20 | 21 | - [Documentation](https://create.t3.gg/) 22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 23 | 24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 25 | 26 | ## How do I deploy this? 27 | 28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 29 | -------------------------------------------------------------------------------- /tooling/prettier/index.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | 3 | /** @typedef {import("prettier").Config} PrettierConfig */ 4 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */ 5 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */ 6 | 7 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */ 8 | const config = { 9 | plugins: [ 10 | "@ianvs/prettier-plugin-sort-imports", 11 | "prettier-plugin-tailwindcss", 12 | ], 13 | tailwindConfig: fileURLToPath( 14 | new URL("../../tooling/tailwind/web.ts", import.meta.url), 15 | ), 16 | tailwindFunctions: ["cn", "cva"], 17 | importOrder: [ 18 | "", 19 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)", 20 | "^(next/(.*)$)|^(next$)", 21 | "^(expo(.*)$)|^(expo$)", 22 | "", 23 | "", 24 | "^@acme", 25 | "^@acme/(.*)$", 26 | "", 27 | "^[.|..|~]", 28 | "^~/", 29 | "^[../]", 30 | "^[./]", 31 | ], 32 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 33 | importOrderTypeScriptVersion: "4.4.0", 34 | overrides: [ 35 | { 36 | files: "*.json.hbs", 37 | options: { 38 | parser: "json", 39 | }, 40 | }, 41 | { 42 | files: "*.js.hbs", 43 | options: { 44 | parser: "babel", 45 | }, 46 | }, 47 | ], 48 | }; 49 | 50 | export default config; 51 | -------------------------------------------------------------------------------- /tooling/eslint/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Since the ecosystem hasn't fully migrated to ESLint's new FlatConfig system yet, 3 | * we "need" to type some of the plugins manually :( 4 | */ 5 | 6 | declare module "eslint-plugin-import" { 7 | import type { Linter, Rule } from "eslint"; 8 | 9 | export const configs: { 10 | recommended: { rules: Linter.RulesRecord }; 11 | }; 12 | export const rules: Record; 13 | } 14 | 15 | declare module "eslint-plugin-react" { 16 | import type { Linter, Rule } from "eslint"; 17 | 18 | export const configs: { 19 | recommended: { rules: Linter.RulesRecord }; 20 | all: { rules: Linter.RulesRecord }; 21 | "jsx-runtime": { rules: Linter.RulesRecord }; 22 | }; 23 | export const rules: Record; 24 | } 25 | 26 | declare module "eslint-plugin-react-hooks" { 27 | import type { Linter, Rule } from "eslint"; 28 | 29 | export const configs: { 30 | recommended: { 31 | rules: { 32 | "rules-of-hooks": Linter.RuleEntry; 33 | "exhaustive-deps": Linter.RuleEntry; 34 | }; 35 | }; 36 | }; 37 | export const rules: Record; 38 | } 39 | 40 | declare module "@next/eslint-plugin-next" { 41 | import type { Linter, Rule } from "eslint"; 42 | 43 | export const configs: { 44 | recommended: { rules: Linter.RulesRecord }; 45 | "core-web-vitals": { rules: Linter.RulesRecord }; 46 | }; 47 | export const rules: Record; 48 | } 49 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/_components/auth-showcase.tsx: -------------------------------------------------------------------------------- 1 | import { getSession } from "@acme/auth"; 2 | import { Button } from "@acme/ui/button"; 3 | import { headers } from "next/headers"; 4 | import { auth } from "@acme/auth"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export async function AuthShowcase() { 8 | const session = await getSession(); 9 | if (!session) { 10 | return ( 11 |
12 | 27 |
28 | ); 29 | } 30 | 31 | return ( 32 |
33 |

34 | Logged in as {session.user.name} 35 |

36 | 37 |
38 | 50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/db", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./src/index.ts" 10 | }, 11 | "./client": { 12 | "types": "./dist/client.d.ts", 13 | "default": "./src/client.ts" 14 | }, 15 | "./schema": { 16 | "types": "./dist/schema.d.ts", 17 | "default": "./src/schema.ts" 18 | } 19 | }, 20 | "license": "MIT", 21 | "scripts": { 22 | "build": "tsc", 23 | "clean": "git clean -xdf .cache .turbo dist node_modules", 24 | "dev": "tsc", 25 | "format": "prettier --check . --ignore-path ../../.gitignore", 26 | "lint": "eslint", 27 | "push": "pnpm with-env drizzle-kit push", 28 | "studio": "pnpm with-env drizzle-kit studio", 29 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 30 | "with-env": "dotenv -e ../../.env --" 31 | }, 32 | "dependencies": { 33 | "@vercel/postgres": "^0.10.0", 34 | "drizzle-orm": "^0.35.1", 35 | "drizzle-zod": "^0.5.1", 36 | "zod": "catalog:" 37 | }, 38 | "devDependencies": { 39 | "@acme/eslint-config": "workspace:*", 40 | "@acme/prettier-config": "workspace:*", 41 | "@acme/tsconfig": "workspace:*", 42 | "dotenv-cli": "^7.4.2", 43 | "drizzle-kit": "^0.26.2", 44 | "eslint": "catalog:", 45 | "prettier": "catalog:", 46 | "typescript": "catalog:" 47 | }, 48 | "prettier": "@acme/prettier-config" 49 | } 50 | -------------------------------------------------------------------------------- /packages/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/auth", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "react-server": "./src/index.rsc.ts", 9 | "default": "./src/index.ts" 10 | }, 11 | "./expo": "./src/expo.ts", 12 | "./middleware": "./src/middleware.ts", 13 | "./client": "./src/client.ts", 14 | "./env": "./env.ts" 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "clean": "git clean -xdf .cache .turbo dist node_modules", 19 | "format": "prettier --check . --ignore-path ../../.gitignore", 20 | "generate": "pnpx @better-auth/cli generate --output ../db/src/auth-schema.ts", 21 | "lint": "eslint", 22 | "typecheck": "tsc --noEmit" 23 | }, 24 | "dependencies": { 25 | "@acme/db": "workspace:*", 26 | "@auth/core": "0.34.2", 27 | "@auth/drizzle-adapter": "1.4.2", 28 | "@better-auth/expo": "1.1.15-beta.7", 29 | "@t3-oss/env-nextjs": "^0.11.1", 30 | "better-auth": "^1.1.15-beta.7", 31 | "next": "^14.2.15", 32 | "next-auth": "5.0.0-beta.20", 33 | "react": "catalog:react18", 34 | "react-dom": "catalog:react18", 35 | "zod": "catalog:" 36 | }, 37 | "devDependencies": { 38 | "@acme/eslint-config": "workspace:*", 39 | "@acme/prettier-config": "workspace:*", 40 | "@acme/tsconfig": "workspace:*", 41 | "eslint": "catalog:", 42 | "prettier": "catalog:", 43 | "tsup": "^8.3.5", 44 | "typescript": "catalog:" 45 | }, 46 | "prettier": "@acme/prettier-config" 47 | } -------------------------------------------------------------------------------- /apps/nextjs/src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | 3 | import { appRouter, createTRPCContext } from "@acme/api"; 4 | import { auth } from "@acme/auth"; 5 | import { NextRequest } from "next/server"; 6 | 7 | export const runtime = "edge"; 8 | 9 | /** 10 | * Configure basic CORS headers 11 | * You should extend this to match your needs 12 | */ 13 | const setCorsHeaders = (res: Response) => { 14 | res.headers.set("Access-Control-Allow-Origin", "*"); 15 | res.headers.set("Access-Control-Request-Method", "*"); 16 | res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST"); 17 | res.headers.set("Access-Control-Allow-Headers", "*"); 18 | }; 19 | 20 | export const OPTIONS = () => { 21 | const response = new Response(null, { 22 | status: 204, 23 | }); 24 | setCorsHeaders(response); 25 | return response; 26 | }; 27 | 28 | const handler = async (req: NextRequest) => { 29 | const session = await auth.api.getSession({ 30 | headers: req.headers, 31 | }) 32 | const response = await fetchRequestHandler({ 33 | endpoint: "/api/trpc", 34 | router: appRouter, 35 | req, 36 | createContext: () => 37 | createTRPCContext({ 38 | session, 39 | headers: req.headers, 40 | }), 41 | onError({ error, path }) { 42 | console.error(`>>> tRPC Error on '${path}'`, error); 43 | }, 44 | }); 45 | 46 | setCorsHeaders(response); 47 | return response; 48 | } 49 | 50 | 51 | export { handler as GET, handler as POST }; 52 | -------------------------------------------------------------------------------- /tooling/tailwind/base.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: ["src/**/*.{ts,tsx}"], 6 | theme: { 7 | extend: { 8 | colors: { 9 | border: "hsl(var(--border))", 10 | input: "hsl(var(--input))", 11 | ring: "hsl(var(--ring))", 12 | background: "hsl(var(--background))", 13 | foreground: "hsl(var(--foreground))", 14 | primary: { 15 | DEFAULT: "hsl(var(--primary))", 16 | foreground: "hsl(var(--primary-foreground))", 17 | }, 18 | secondary: { 19 | DEFAULT: "hsl(var(--secondary))", 20 | foreground: "hsl(var(--secondary-foreground))", 21 | }, 22 | destructive: { 23 | DEFAULT: "hsl(var(--destructive))", 24 | foreground: "hsl(var(--destructive-foreground))", 25 | }, 26 | muted: { 27 | DEFAULT: "hsl(var(--muted))", 28 | foreground: "hsl(var(--muted-foreground))", 29 | }, 30 | accent: { 31 | DEFAULT: "hsl(var(--accent))", 32 | foreground: "hsl(var(--accent-foreground))", 33 | }, 34 | popover: { 35 | DEFAULT: "hsl(var(--popover))", 36 | foreground: "hsl(var(--popover-foreground))", 37 | }, 38 | card: { 39 | DEFAULT: "hsl(var(--card))", 40 | foreground: "hsl(var(--card-foreground))", 41 | }, 42 | }, 43 | borderColor: { 44 | DEFAULT: "hsl(var(--border))", 45 | }, 46 | }, 47 | }, 48 | } satisfies Config; 49 | -------------------------------------------------------------------------------- /apps/expo/src/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: 0 0% 100%; 7 | --foreground: 240 10% 3.9%; 8 | --card: 0 0% 100%; 9 | --card-foreground: 240 10% 3.9%; 10 | --popover: 0 0% 100%; 11 | --popover-foreground: 240 10% 3.9%; 12 | --primary: 327 66% 69%; 13 | --primary-foreground: 337 65.5% 17.1%; 14 | --secondary: 240 4.8% 95.9%; 15 | --secondary-foreground: 240 5.9% 10%; 16 | --muted: 240 4.8% 95.9%; 17 | --muted-foreground: 240 3.8% 46.1%; 18 | --accent: 240 4.8% 95.9%; 19 | --accent-foreground: 240 5.9% 10%; 20 | --destructive: 0 72.22% 50.59%; 21 | --destructive-foreground: 0 0% 98%; 22 | --border: 240 5.9% 90%; 23 | --input: 240 5.9% 90%; 24 | --ring: 240 5% 64.9%; 25 | --radius: 0.5rem; 26 | } 27 | 28 | @media (prefers-color-scheme: dark) { 29 | :root { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 327 66% 69%; 37 | --primary-foreground: 337 65.5% 17.1%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 327 66% 69%; 14 | --primary-foreground: 337 65.5% 17.1%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 72.22% 50.59%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 5% 64.9%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 240 10% 3.9%; 31 | --foreground: 0 0% 98%; 32 | --card: 240 10% 3.9%; 33 | --card-foreground: 0 0% 98%; 34 | --popover: 240 10% 3.9%; 35 | --popover-foreground: 0 0% 98%; 36 | --primary: 327 66% 69%; 37 | --primary-foreground: 337 65.5% 17.1%; 38 | --secondary: 240 3.7% 15.9%; 39 | --secondary-foreground: 0 0% 98%; 40 | --muted: 240 3.7% 15.9%; 41 | --muted-foreground: 240 5% 64.9%; 42 | --accent: 240 3.7% 15.9%; 43 | --accent-foreground: 0 0% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 0 85.7% 97.3%; 46 | --border: 240 3.7% 15.9%; 47 | --input: 240 3.7% 15.9%; 48 | --ring: 240 4.9% 83.9%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "pnpm with-env next build", 8 | "clean": "git clean -xdf .cache .next .turbo node_modules", 9 | "dev": "pnpm with-env next dev", 10 | "format": "prettier --check . --ignore-path ../../.gitignore", 11 | "lint": "eslint", 12 | "start": "pnpm with-env next start", 13 | "typecheck": "tsc --noEmit", 14 | "with-env": "dotenv -e ../../.env --" 15 | }, 16 | "dependencies": { 17 | "@acme/api": "workspace:*", 18 | "@acme/auth": "workspace:*", 19 | "@acme/db": "workspace:*", 20 | "@acme/ui": "workspace:*", 21 | "@acme/validators": "workspace:*", 22 | "@t3-oss/env-nextjs": "^0.11.1", 23 | "@tanstack/react-query": "catalog:", 24 | "@trpc/client": "catalog:", 25 | "@trpc/react-query": "catalog:", 26 | "@trpc/server": "catalog:", 27 | "geist": "^1.3.1", 28 | "next": "^14.2.15", 29 | "react": "catalog:react18", 30 | "react-dom": "catalog:react18", 31 | "superjson": "2.2.1", 32 | "zod": "catalog:" 33 | }, 34 | "devDependencies": { 35 | "@acme/eslint-config": "workspace:*", 36 | "@acme/prettier-config": "workspace:*", 37 | "@acme/tailwind-config": "workspace:*", 38 | "@acme/tsconfig": "workspace:*", 39 | "@types/node": "^20.16.11", 40 | "@types/react": "catalog:react18", 41 | "@types/react-dom": "catalog:react18", 42 | "dotenv-cli": "^7.4.2", 43 | "eslint": "catalog:", 44 | "jiti": "^1.21.6", 45 | "prettier": "catalog:", 46 | "tailwindcss": "catalog:", 47 | "typescript": "catalog:" 48 | }, 49 | "prettier": "@acme/prettier-config" 50 | } -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "topo": { 6 | "dependsOn": [ 7 | "^topo" 8 | ] 9 | }, 10 | "build": { 11 | "dependsOn": [ 12 | "^build" 13 | ], 14 | "outputs": [ 15 | ".cache/tsbuildinfo.json", 16 | "dist/**" 17 | ] 18 | }, 19 | "dev": { 20 | "dependsOn": [ 21 | "^dev" 22 | ], 23 | "cache": false, 24 | "persistent": false 25 | }, 26 | "format": { 27 | "outputs": [ 28 | ".cache/.prettiercache" 29 | ], 30 | "outputLogs": "new-only" 31 | }, 32 | "lint": { 33 | "dependsOn": [ 34 | "^topo", 35 | "^build" 36 | ], 37 | "outputs": [ 38 | ".cache/.eslintcache" 39 | ] 40 | }, 41 | "typecheck": { 42 | "dependsOn": [ 43 | "^topo", 44 | "^build" 45 | ], 46 | "outputs": [ 47 | ".cache/tsbuildinfo.json" 48 | ] 49 | }, 50 | "clean": { 51 | "cache": false 52 | }, 53 | "//#clean": { 54 | "cache": false 55 | }, 56 | "push": { 57 | "cache": false, 58 | "interactive": true 59 | }, 60 | "studio": { 61 | "cache": false, 62 | "persistent": true 63 | }, 64 | "ui-add": { 65 | "cache": false, 66 | "interactive": true 67 | } 68 | }, 69 | "globalEnv": [ 70 | "POSTGRES_URL", 71 | "AUTH_DISCORD_ID", 72 | "AUTH_DISCORD_SECRET", 73 | "AUTH_REDIRECT_PROXY_URL", 74 | "AUTH_SECRET", 75 | "PORT" 76 | ], 77 | "globalPassThroughEnv": [ 78 | "NODE_ENV", 79 | "CI", 80 | "VERCEL", 81 | "VERCEL_ENV", 82 | "VERCEL_URL", 83 | "npm_lifecycle_event" 84 | ] 85 | } -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/ui", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/src/index.d.ts", 9 | "default": "./src/index.ts" 10 | }, 11 | "./*": { 12 | "types": "./dist/src/*.d.ts", 13 | "default": [ 14 | "./src/*.ts", 15 | "./src/*.tsx" 16 | ] 17 | } 18 | }, 19 | "license": "MIT", 20 | "scripts": { 21 | "build": "tsc", 22 | "clean": "git clean -xdf .cache .turbo dist node_modules", 23 | "dev": "tsc", 24 | "format": "prettier --check . --ignore-path ../../.gitignore", 25 | "lint": "eslint", 26 | "typecheck": "tsc --noEmit --emitDeclarationOnly false", 27 | "ui-add": "pnpm dlx shadcn@latest add && prettier src --write --list-different" 28 | }, 29 | "dependencies": { 30 | "@hookform/resolvers": "^3.9.0", 31 | "@radix-ui/react-dropdown-menu": "^2.1.2", 32 | "@radix-ui/react-icons": "^1.3.0", 33 | "@radix-ui/react-label": "^2.1.0", 34 | "@radix-ui/react-slot": "^1.1.0", 35 | "class-variance-authority": "^0.7.0", 36 | "next-themes": "^0.3.0", 37 | "react-hook-form": "^7.53.0", 38 | "sonner": "^1.5.0", 39 | "tailwind-merge": "^2.5.4" 40 | }, 41 | "devDependencies": { 42 | "@acme/eslint-config": "workspace:*", 43 | "@acme/prettier-config": "workspace:*", 44 | "@acme/tailwind-config": "workspace:*", 45 | "@acme/tsconfig": "workspace:*", 46 | "@types/react": "catalog:react18", 47 | "eslint": "catalog:", 48 | "prettier": "catalog:", 49 | "react": "catalog:react18", 50 | "typescript": "catalog:", 51 | "zod": "catalog:" 52 | }, 53 | "peerDependencies": { 54 | "react": "catalog:react18", 55 | "zod": "catalog:" 56 | }, 57 | "prettier": "@acme/prettier-config" 58 | } 59 | -------------------------------------------------------------------------------- /packages/db/src/auth-schema.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"; 2 | 3 | export const user = pgTable("user", { 4 | id: text("id").primaryKey(), 5 | name: text('name').notNull(), 6 | email: text('email').notNull().unique(), 7 | emailVerified: boolean('email_verified').notNull(), 8 | image: text('image'), 9 | createdAt: timestamp('created_at').notNull(), 10 | updatedAt: timestamp('updated_at').notNull() 11 | }); 12 | 13 | export const session = pgTable("session", { 14 | id: text("id").primaryKey(), 15 | expiresAt: timestamp('expires_at').notNull(), 16 | token: text('token').notNull().unique(), 17 | createdAt: timestamp('created_at').notNull(), 18 | updatedAt: timestamp('updated_at').notNull(), 19 | ipAddress: text('ip_address'), 20 | userAgent: text('user_agent'), 21 | userId: text('user_id').notNull().references(() => user.id) 22 | }); 23 | 24 | export const account = pgTable("account", { 25 | id: text("id").primaryKey(), 26 | accountId: text('account_id').notNull(), 27 | providerId: text('provider_id').notNull(), 28 | userId: text('user_id').notNull().references(() => user.id), 29 | accessToken: text('access_token'), 30 | refreshToken: text('refresh_token'), 31 | idToken: text('id_token'), 32 | accessTokenExpiresAt: timestamp('access_token_expires_at'), 33 | refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), 34 | scope: text('scope'), 35 | password: text('password'), 36 | createdAt: timestamp('created_at').notNull(), 37 | updatedAt: timestamp('updated_at').notNull() 38 | }); 39 | 40 | export const verification = pgTable("verification", { 41 | id: text("id").primaryKey(), 42 | identifier: text('identifier').notNull(), 43 | value: text('value').notNull(), 44 | expiresAt: timestamp('expires_at').notNull(), 45 | createdAt: timestamp('created_at'), 46 | updatedAt: timestamp('updated_at') 47 | }); 48 | -------------------------------------------------------------------------------- /apps/expo/src/utils/api.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | import { httpBatchLink, loggerLink } from "@trpc/client"; 4 | import { createTRPCReact } from "@trpc/react-query"; 5 | import superjson from "superjson"; 6 | 7 | import type { AppRouter } from "@acme/api"; 8 | 9 | import { getBaseUrl } from "./base-url"; 10 | import { getToken } from "./session-store"; 11 | 12 | /** 13 | * A set of typesafe hooks for consuming your API. 14 | */ 15 | export const api = createTRPCReact(); 16 | export { type RouterInputs, type RouterOutputs } from "@acme/api"; 17 | 18 | /** 19 | * A wrapper for your app that provides the TRPC context. 20 | * Use only in _app.tsx 21 | */ 22 | export function TRPCProvider(props: { children: React.ReactNode }) { 23 | const [queryClient] = useState(() => new QueryClient()); 24 | const [trpcClient] = useState(() => 25 | api.createClient({ 26 | links: [ 27 | loggerLink({ 28 | enabled: (opts) => 29 | process.env.NODE_ENV === "development" || 30 | (opts.direction === "down" && opts.result instanceof Error), 31 | colorMode: "ansi", 32 | }), 33 | httpBatchLink({ 34 | transformer: superjson, 35 | url: `${getBaseUrl()}/api/trpc`, 36 | headers() { 37 | const headers = new Map(); 38 | headers.set("x-trpc-source", "expo-react"); 39 | 40 | const token = getToken(); 41 | if (token) headers.set("Authorization", `Bearer ${token}`); 42 | 43 | return Object.fromEntries(headers); 44 | }, 45 | }), 46 | ], 47 | }), 48 | ); 49 | 50 | return ( 51 | 52 | 53 | {props.children} 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { GeistMono } from "geist/font/mono"; 3 | import { GeistSans } from "geist/font/sans"; 4 | 5 | import { cn } from "@acme/ui"; 6 | import { ThemeProvider, ThemeToggle } from "@acme/ui/theme"; 7 | import { Toaster } from "@acme/ui/toast"; 8 | 9 | import { TRPCReactProvider } from "~/trpc/react"; 10 | 11 | import "~/app/globals.css"; 12 | 13 | import { env } from "~/env"; 14 | 15 | export const metadata: Metadata = { 16 | metadataBase: new URL( 17 | env.VERCEL_ENV === "production" 18 | ? "https://turbo.t3.gg" 19 | : "http://localhost:3000", 20 | ), 21 | title: "Create T3 Turbo", 22 | description: "Simple monorepo with shared backend for web & mobile apps", 23 | openGraph: { 24 | title: "Create T3 Turbo", 25 | description: "Simple monorepo with shared backend for web & mobile apps", 26 | url: "https://create-t3-turbo.vercel.app", 27 | siteName: "Create T3 Turbo", 28 | }, 29 | twitter: { 30 | card: "summary_large_image", 31 | site: "@jullerino", 32 | creator: "@jullerino", 33 | }, 34 | }; 35 | 36 | export const viewport: Viewport = { 37 | themeColor: [ 38 | { media: "(prefers-color-scheme: light)", color: "white" }, 39 | { media: "(prefers-color-scheme: dark)", color: "black" }, 40 | ], 41 | }; 42 | 43 | export default function RootLayout(props: { children: React.ReactNode }) { 44 | return ( 45 | 46 | 53 | 54 | {props.children} 55 |
56 | 57 |
58 | 59 |
60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /packages/ui/src/button.tsx: -------------------------------------------------------------------------------- 1 | import type { VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { cva } from "class-variance-authority"; 5 | 6 | import { cn } from "@acme/ui"; 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 10 | { 11 | variants: { 12 | variant: { 13 | primary: 14 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 15 | destructive: 16 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 17 | outline: 18 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 19 | secondary: 20 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 21 | ghost: "hover:bg-accent hover:text-accent-foreground", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | md: "h-9 px-4 py-2", 27 | lg: "h-10 rounded-md px-8", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "primary", 33 | size: "md", 34 | }, 35 | }, 36 | ); 37 | 38 | interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button"; 47 | return ( 48 | 53 | ); 54 | }, 55 | ); 56 | Button.displayName = "Button"; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /apps/expo/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more: https://docs.expo.dev/guides/monorepos/ 2 | const { getDefaultConfig } = require("expo/metro-config"); 3 | const { FileStore } = require("metro-cache"); 4 | const { withNativeWind } = require("nativewind/metro"); 5 | 6 | const path = require("path"); 7 | 8 | const config = withTurborepoManagedCache( 9 | withMonorepoPaths( 10 | withNativeWind(getDefaultConfig(__dirname), { 11 | input: "./src/styles.css", 12 | configPath: "./tailwind.config.ts", 13 | }), 14 | ), 15 | ); 16 | 17 | // XXX: Resolve our exports in workspace packages 18 | // https://github.com/expo/expo/issues/26926 19 | config.resolver.unstable_enablePackageExports = true; 20 | 21 | module.exports = config; 22 | 23 | /** 24 | * Add the monorepo paths to the Metro config. 25 | * This allows Metro to resolve modules from the monorepo. 26 | * 27 | * @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config 28 | * @param {import('expo/metro-config').MetroConfig} config 29 | * @returns {import('expo/metro-config').MetroConfig} 30 | */ 31 | function withMonorepoPaths(config) { 32 | const projectRoot = __dirname; 33 | const workspaceRoot = path.resolve(projectRoot, "../.."); 34 | 35 | // #1 - Watch all files in the monorepo 36 | config.watchFolders = [workspaceRoot]; 37 | 38 | // #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules 39 | config.resolver.nodeModulesPaths = [ 40 | path.resolve(projectRoot, "node_modules"), 41 | path.resolve(workspaceRoot, "node_modules"), 42 | ]; 43 | 44 | return config; 45 | } 46 | 47 | /** 48 | * Move the Metro cache to the `.cache/metro` folder. 49 | * If you have any environment variables, you can configure Turborepo to invalidate it when needed. 50 | * 51 | * @see https://turbo.build/repo/docs/reference/configuration#env 52 | * @param {import('expo/metro-config').MetroConfig} config 53 | * @returns {import('expo/metro-config').MetroConfig} 54 | */ 55 | function withTurborepoManagedCache(config) { 56 | config.cacheStores = [ 57 | new FileStore({ root: path.join(__dirname, ".cache/metro") }), 58 | ]; 59 | return config; 60 | } 61 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/react.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { QueryClient } from "@tanstack/react-query"; 4 | import { useState } from "react"; 5 | import { QueryClientProvider } from "@tanstack/react-query"; 6 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; 7 | import { createTRPCReact } from "@trpc/react-query"; 8 | import SuperJSON from "superjson"; 9 | 10 | import type { AppRouter } from "@acme/api"; 11 | 12 | import { env } from "~/env"; 13 | import { createQueryClient } from "./query-client"; 14 | 15 | let clientQueryClientSingleton: QueryClient | undefined = undefined; 16 | const getQueryClient = () => { 17 | if (typeof window === "undefined") { 18 | // Server: always make a new query client 19 | return createQueryClient(); 20 | } else { 21 | // Browser: use singleton pattern to keep the same query client 22 | return (clientQueryClientSingleton ??= createQueryClient()); 23 | } 24 | }; 25 | 26 | export const api = createTRPCReact(); 27 | 28 | export function TRPCReactProvider(props: { children: React.ReactNode }) { 29 | const queryClient = getQueryClient(); 30 | 31 | const [trpcClient] = useState(() => 32 | api.createClient({ 33 | links: [ 34 | loggerLink({ 35 | enabled: (op) => 36 | env.NODE_ENV === "development" || 37 | (op.direction === "down" && op.result instanceof Error), 38 | }), 39 | unstable_httpBatchStreamLink({ 40 | transformer: SuperJSON, 41 | url: getBaseUrl() + "/api/trpc", 42 | headers() { 43 | const headers = new Headers(); 44 | headers.set("x-trpc-source", "nextjs-react"); 45 | return headers; 46 | }, 47 | }), 48 | ], 49 | }), 50 | ); 51 | 52 | return ( 53 | 54 | 55 | {props.children} 56 | 57 | 58 | ); 59 | } 60 | 61 | const getBaseUrl = () => { 62 | if (typeof window !== "undefined") return window.location.origin; 63 | if (env.VERCEL_URL) return `https://${env.VERCEL_URL}`; 64 | // eslint-disable-next-line no-restricted-properties 65 | return `http://localhost:${process.env.PORT ?? 3000}`; 66 | }; 67 | -------------------------------------------------------------------------------- /apps/expo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/expo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "index.ts", 6 | "scripts": { 7 | "clean": "git clean -xdf .cache .expo .turbo android ios node_modules", 8 | "dev": "expo start", 9 | "dev:android": "expo start --android", 10 | "dev:ios": "expo start --ios", 11 | "android": "expo run:android", 12 | "ios": "expo run:ios", 13 | "format": "prettier --check . --ignore-path ../../.gitignore", 14 | "lint": "eslint", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@bacons/text-decoder": "^0.0.0", 19 | "@better-auth/expo": "^1.1.15-beta.7", 20 | "@better-fetch/fetch": "^1.1.12", 21 | "@expo/metro-config": "^0.19.9", 22 | "@shopify/flash-list": "1.7.1", 23 | "@tanstack/react-query": "catalog:", 24 | "@trpc/client": "catalog:", 25 | "@trpc/react-query": "catalog:", 26 | "@trpc/server": "catalog:", 27 | "better-auth": "^1.1.15-beta.7", 28 | "expo": "~52.0.28", 29 | "expo-constants": "~17.0.5", 30 | "expo-dev-client": "~5.0.10", 31 | "expo-linking": "~7.0.5", 32 | "expo-router": "~4.0.17", 33 | "expo-secure-store": "^14.0.1", 34 | "expo-splash-screen": "~0.29.21", 35 | "expo-status-bar": "~2.0.1", 36 | "expo-web-browser": "^14.0.2", 37 | "nativewind": "~4.0.36", 38 | "react": "catalog:react18", 39 | "react-dom": "catalog:react18", 40 | "react-native": "~0.76.6", 41 | "react-native-css-interop": "~0.0.36", 42 | "react-native-gesture-handler": "~2.20.2", 43 | "react-native-reanimated": "~3.16.7", 44 | "react-native-safe-area-context": "~4.12.0", 45 | "react-native-screens": "~4.4.0", 46 | "superjson": "2.2.1" 47 | }, 48 | "devDependencies": { 49 | "@acme/api": "workspace:*", 50 | "@acme/eslint-config": "workspace:*", 51 | "@acme/prettier-config": "workspace:*", 52 | "@acme/tailwind-config": "workspace:*", 53 | "@acme/tsconfig": "workspace:*", 54 | "@babel/core": "^7.25.8", 55 | "@babel/preset-env": "^7.25.8", 56 | "@babel/runtime": "^7.25.7", 57 | "@types/babel__core": "^7.20.5", 58 | "@types/react": "catalog:react18", 59 | "eslint": "catalog:", 60 | "prettier": "catalog:", 61 | "tailwindcss": "catalog:", 62 | "typescript": "catalog:" 63 | }, 64 | "prettier": "@acme/prettier-config" 65 | } -------------------------------------------------------------------------------- /tooling/eslint/base.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as path from "node:path"; 4 | import { includeIgnoreFile } from "@eslint/compat"; 5 | import eslint from "@eslint/js"; 6 | import importPlugin from "eslint-plugin-import"; 7 | import turboPlugin from "eslint-plugin-turbo"; 8 | import tseslint from "typescript-eslint"; 9 | 10 | /** 11 | * All packages that leverage t3-env should use this rule 12 | */ 13 | export const restrictEnvAccess = tseslint.config( 14 | { ignores: ["**/env.ts"] }, 15 | { 16 | files: ["**/*.js", "**/*.ts", "**/*.tsx"], 17 | rules: { 18 | "no-restricted-properties": [ 19 | "error", 20 | { 21 | object: "process", 22 | property: "env", 23 | message: 24 | "Use `import { env } from '~/env'` instead to ensure validated types.", 25 | }, 26 | ], 27 | "no-restricted-imports": [ 28 | "error", 29 | { 30 | name: "process", 31 | importNames: ["env"], 32 | message: 33 | "Use `import { env } from '~/env'` instead to ensure validated types.", 34 | }, 35 | ], 36 | }, 37 | }, 38 | ); 39 | 40 | export default tseslint.config( 41 | // Ignore files not tracked by VCS and any config files 42 | includeIgnoreFile(path.join(import.meta.dirname, "../../.gitignore")), 43 | { ignores: ["**/*.config.*"] }, 44 | { 45 | files: ["**/*.js", "**/*.ts", "**/*.tsx"], 46 | plugins: { 47 | import: importPlugin, 48 | turbo: turboPlugin, 49 | }, 50 | extends: [ 51 | eslint.configs.recommended, 52 | ...tseslint.configs.recommended, 53 | ...tseslint.configs.recommendedTypeChecked, 54 | ...tseslint.configs.stylisticTypeChecked, 55 | ], 56 | rules: { 57 | ...turboPlugin.configs.recommended.rules, 58 | "@typescript-eslint/no-unused-vars": [ 59 | "error", 60 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, 61 | ], 62 | "@typescript-eslint/consistent-type-imports": [ 63 | "warn", 64 | { prefer: "type-imports", fixStyle: "separate-type-imports" }, 65 | ], 66 | "@typescript-eslint/no-misused-promises": [ 67 | 2, 68 | { checksVoidReturn: { attributes: false } }, 69 | ], 70 | "@typescript-eslint/no-unnecessary-condition": [ 71 | "error", 72 | { 73 | allowConstantLoopConditions: true, 74 | }, 75 | ], 76 | "@typescript-eslint/no-non-null-assertion": "error", 77 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"], 78 | }, 79 | }, 80 | { 81 | linterOptions: { reportUnusedDisableDirectives: true }, 82 | languageOptions: { parserOptions: { projectService: true } }, 83 | }, 84 | ); 85 | -------------------------------------------------------------------------------- /turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | import type { PlopTypes } from "@turbo/gen"; 3 | 4 | interface PackageJson { 5 | name: string; 6 | scripts: Record; 7 | dependencies: Record; 8 | devDependencies: Record; 9 | } 10 | 11 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 12 | plop.setGenerator("init", { 13 | description: "Generate a new package for the Acme Monorepo", 14 | prompts: [ 15 | { 16 | type: "input", 17 | name: "name", 18 | message: 19 | "What is the name of the package? (You can skip the `@acme/` prefix)", 20 | }, 21 | { 22 | type: "input", 23 | name: "deps", 24 | message: 25 | "Enter a space separated list of dependencies you would like to install", 26 | }, 27 | ], 28 | actions: [ 29 | (answers) => { 30 | if ("name" in answers && typeof answers.name === "string") { 31 | if (answers.name.startsWith("@acme/")) { 32 | answers.name = answers.name.replace("@acme/", ""); 33 | } 34 | } 35 | return "Config sanitized"; 36 | }, 37 | { 38 | type: "add", 39 | path: "packages/{{ name }}/eslint.config.js", 40 | templateFile: "templates/eslint.config.js.hbs", 41 | }, 42 | { 43 | type: "add", 44 | path: "packages/{{ name }}/package.json", 45 | templateFile: "templates/package.json.hbs", 46 | }, 47 | { 48 | type: "add", 49 | path: "packages/{{ name }}/tsconfig.json", 50 | templateFile: "templates/tsconfig.json.hbs", 51 | }, 52 | { 53 | type: "add", 54 | path: "packages/{{ name }}/src/index.ts", 55 | template: "export const name = '{{ name }}';", 56 | }, 57 | { 58 | type: "modify", 59 | path: "packages/{{ name }}/package.json", 60 | async transform(content, answers) { 61 | if ("deps" in answers && typeof answers.deps === "string") { 62 | const pkg = JSON.parse(content) as PackageJson; 63 | for (const dep of answers.deps.split(" ").filter(Boolean)) { 64 | const version = await fetch( 65 | `https://registry.npmjs.org/-/package/${dep}/dist-tags`, 66 | ) 67 | .then((res) => res.json()) 68 | .then((json) => json.latest); 69 | if (!pkg.dependencies) pkg.dependencies = {}; 70 | pkg.dependencies[dep] = `^${version}`; 71 | } 72 | return JSON.stringify(pkg, null, 2); 73 | } 74 | return content; 75 | }, 76 | }, 77 | async (answers) => { 78 | /** 79 | * Install deps and format everything 80 | */ 81 | if ("name" in answers && typeof answers.name === "string") { 82 | // execSync("pnpm dlx sherif@latest --fix", { 83 | // stdio: "inherit", 84 | // }); 85 | execSync("pnpm i", { stdio: "inherit" }); 86 | execSync( 87 | `pnpm prettier --write packages/${answers.name}/** --list-different`, 88 | ); 89 | return "Package scaffolded"; 90 | } 91 | return "Package not scaffolded"; 92 | }, 93 | ], 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /packages/api/src/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: 3 | * 1. You want to modify request context (see Part 1) 4 | * 2. You want to create a new middleware or type of procedure (see Part 3) 5 | * 6 | * tl;dr - this is where all the tRPC server stuff is created and plugged in. 7 | * The pieces you will need to use are documented accordingly near the end 8 | */ 9 | import { initTRPC, TRPCError } from "@trpc/server"; 10 | import superjson from "superjson"; 11 | import { ZodError } from "zod"; 12 | import { db } from "@acme/db/client"; 13 | import { auth } from "@acme/auth"; 14 | 15 | /** 16 | * 1. CONTEXT 17 | * 18 | * This section defines the "contexts" that are available in the backend API. 19 | * 20 | * These allow you to access things when processing a request, like the database, the session, etc. 21 | * 22 | * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each 23 | * wrap this and provides the required context. 24 | * 25 | * @see https://trpc.io/docs/server/context 26 | */ 27 | export const createTRPCContext = async (opts: { 28 | headers: Headers; 29 | }) => { 30 | const session = await auth.api.getSession({ 31 | headers: opts.headers, 32 | }) 33 | return { 34 | session, 35 | db, 36 | }; 37 | }; 38 | 39 | /** 40 | * 2. INITIALIZATION 41 | * 42 | * This is where the trpc api is initialized, connecting the context and 43 | * transformer 44 | */ 45 | const t = initTRPC.context().create({ 46 | transformer: superjson, 47 | errorFormatter: ({ shape, error }) => ({ 48 | ...shape, 49 | data: { 50 | ...shape.data, 51 | zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, 52 | }, 53 | }), 54 | }); 55 | 56 | /** 57 | * Create a server-side caller 58 | * @see https://trpc.io/docs/server/server-side-calls 59 | */ 60 | export const createCallerFactory = t.createCallerFactory; 61 | 62 | /** 63 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) 64 | * 65 | * These are the pieces you use to build your tRPC API. You should import these 66 | * a lot in the /src/server/api/routers folder 67 | */ 68 | 69 | /** 70 | * This is how you create new routers and subrouters in your tRPC API 71 | * @see https://trpc.io/docs/router 72 | */ 73 | export const createTRPCRouter = t.router; 74 | 75 | /** 76 | * Middleware for timing procedure execution and adding an articifial delay in development. 77 | * 78 | * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating 79 | * network latency that would occur in production but not in local development. 80 | */ 81 | const timingMiddleware = t.middleware(async ({ next, path }) => { 82 | const start = Date.now(); 83 | 84 | if (t._config.isDev) { 85 | // artificial delay in dev 100-500ms 86 | const waitMs = Math.floor(Math.random() * 400) + 100; 87 | await new Promise((resolve) => setTimeout(resolve, waitMs)); 88 | } 89 | 90 | const result = await next(); 91 | 92 | const end = Date.now(); 93 | console.log(`[TRPC] ${path} took ${end - start}ms to execute`); 94 | 95 | return result; 96 | }); 97 | 98 | /** 99 | * Public (unauthed) procedure 100 | * 101 | * This is the base piece you use to build new queries and mutations on your 102 | * tRPC API. It does not guarantee that a user querying is authorized, but you 103 | * can still access user session data if they are logged in 104 | */ 105 | export const publicProcedure = t.procedure.use(timingMiddleware); 106 | 107 | /** 108 | * Protected (authenticated) procedure 109 | * 110 | * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies 111 | * the session is valid and guarantees `ctx.session.user` is not null. 112 | * 113 | * @see https://trpc.io/docs/procedures 114 | */ 115 | export const protectedProcedure = t.procedure 116 | .use(timingMiddleware) 117 | .use(({ ctx, next }) => { 118 | if (!ctx.session?.user) { 119 | throw new TRPCError({ code: "UNAUTHORIZED" }); 120 | } 121 | return next({ 122 | ctx: { 123 | // infers the `session` as non-nullable 124 | session: { ...ctx.session, user: ctx.session.user }, 125 | }, 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /apps/expo/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button, Pressable, Text, TextInput, View } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | import { Link, Stack } from "expo-router"; 5 | import { FlashList } from "@shopify/flash-list"; 6 | 7 | import type { RouterOutputs } from "~/utils/api"; 8 | import { api } from "~/utils/api"; 9 | import { authClient, signIn, signOut } from "~/utils/auth"; 10 | 11 | function PostCard(props: { 12 | post: RouterOutputs["post"]["all"][number]; 13 | onDelete: () => void; 14 | }) { 15 | return ( 16 | 17 | 18 | 25 | 26 | 27 | {props.post.title} 28 | 29 | {props.post.content} 30 | 31 | 32 | 33 | 34 | Delete 35 | 36 | 37 | ); 38 | } 39 | 40 | function CreatePost() { 41 | const utils = api.useUtils(); 42 | 43 | const [title, setTitle] = useState(""); 44 | const [content, setContent] = useState(""); 45 | 46 | const { mutate, error } = api.post.create.useMutation({ 47 | async onSuccess() { 48 | setTitle(""); 49 | setContent(""); 50 | await utils.post.all.invalidate(); 51 | }, 52 | }); 53 | 54 | return ( 55 | 56 | 62 | {error?.data?.zodError?.fieldErrors.title && ( 63 | 64 | {error.data.zodError.fieldErrors.title} 65 | 66 | )} 67 | 73 | {error?.data?.zodError?.fieldErrors.content && ( 74 | 75 | {error.data.zodError.fieldErrors.content} 76 | 77 | )} 78 | { 81 | mutate({ 82 | title, 83 | content, 84 | }); 85 | }} 86 | > 87 | Create 88 | 89 | {error?.data?.code === "UNAUTHORIZED" && ( 90 | 91 | You need to be logged in to create a post 92 | 93 | )} 94 | 95 | ); 96 | } 97 | 98 | function MobileAuth() { 99 | const { data: session } = authClient.useSession(); 100 | return ( 101 | <> 102 | 103 | {session?.user.name ?? "Not logged in"} 104 | 105 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export function PostList() { 83 | const [posts] = api.post.all.useSuspenseQuery(); 84 | 85 | if (posts.length === 0) { 86 | return ( 87 |
88 | 89 | 90 | 91 | 92 |
93 |

No posts yet

94 |
95 |
96 | ); 97 | } 98 | 99 | return ( 100 |
101 | {posts.map((p) => { 102 | return ; 103 | })} 104 |
105 | ); 106 | } 107 | 108 | export function PostCard(props: { 109 | post: RouterOutputs["post"]["all"][number]; 110 | }) { 111 | const utils = api.useUtils(); 112 | const deletePost = api.post.delete.useMutation({ 113 | onSuccess: async () => { 114 | await utils.post.invalidate(); 115 | }, 116 | onError: (err) => { 117 | toast.error( 118 | err.data?.code === "UNAUTHORIZED" 119 | ? "You must be logged in to delete a post" 120 | : "Failed to delete post", 121 | ); 122 | }, 123 | }); 124 | 125 | return ( 126 |
127 |
128 |

{props.post.title}

129 |

{props.post.content}

130 |
131 |
132 | 139 |
140 |
141 | ); 142 | } 143 | 144 | export function PostCardSkeleton(props: { pulse?: boolean }) { 145 | const { pulse = true } = props; 146 | return ( 147 |
148 |
149 |

155 |   156 |

157 |

163 |   164 |

165 |
166 |
167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /packages/ui/src/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as LabelPrimitive from "@radix-ui/react-label"; 4 | import type { 5 | ControllerProps, 6 | FieldPath, 7 | FieldValues, 8 | UseFormProps, 9 | } from "react-hook-form"; 10 | import type { ZodType, ZodTypeDef } from "zod"; 11 | import * as React from "react"; 12 | import { zodResolver } from "@hookform/resolvers/zod"; 13 | import { Slot } from "@radix-ui/react-slot"; 14 | import { 15 | useForm as __useForm, 16 | Controller, 17 | FormProvider, 18 | useFormContext, 19 | } from "react-hook-form"; 20 | 21 | import { cn } from "@acme/ui"; 22 | 23 | import { Label } from "./label"; 24 | 25 | const useForm = < 26 | TOut extends FieldValues, 27 | TDef extends ZodTypeDef, 28 | TIn extends FieldValues, 29 | >( 30 | props: Omit, "resolver"> & { 31 | schema: ZodType; 32 | }, 33 | ) => { 34 | const form = __useForm({ 35 | ...props, 36 | resolver: zodResolver(props.schema, undefined), 37 | }); 38 | 39 | return form; 40 | }; 41 | 42 | const Form = FormProvider; 43 | 44 | interface FormFieldContextValue< 45 | TFieldValues extends FieldValues = FieldValues, 46 | TName extends FieldPath = FieldPath, 47 | > { 48 | name: TName; 49 | } 50 | 51 | const FormFieldContext = React.createContext( 52 | null, 53 | ); 54 | 55 | const FormField = < 56 | TFieldValues extends FieldValues = FieldValues, 57 | TName extends FieldPath = FieldPath, 58 | >({ 59 | ...props 60 | }: ControllerProps) => { 61 | return ( 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | const useFormField = () => { 69 | const fieldContext = React.useContext(FormFieldContext); 70 | const itemContext = React.useContext(FormItemContext); 71 | const { getFieldState, formState } = useFormContext(); 72 | 73 | if (!fieldContext) { 74 | throw new Error("useFormField should be used within "); 75 | } 76 | const fieldState = getFieldState(fieldContext.name, formState); 77 | 78 | const { id } = itemContext; 79 | 80 | return { 81 | id, 82 | name: fieldContext.name, 83 | formItemId: `${id}-form-item`, 84 | formDescriptionId: `${id}-form-item-description`, 85 | formMessageId: `${id}-form-item-message`, 86 | ...fieldState, 87 | }; 88 | }; 89 | 90 | interface FormItemContextValue { 91 | id: string; 92 | } 93 | 94 | const FormItemContext = React.createContext( 95 | {} as FormItemContextValue, 96 | ); 97 | 98 | const FormItem = React.forwardRef< 99 | HTMLDivElement, 100 | React.HTMLAttributes 101 | >(({ className, ...props }, ref) => { 102 | const id = React.useId(); 103 | 104 | return ( 105 | 106 |
107 | 108 | ); 109 | }); 110 | FormItem.displayName = "FormItem"; 111 | 112 | const FormLabel = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, ...props }, ref) => { 116 | const { error, formItemId } = useFormField(); 117 | 118 | return ( 119 |