diff --git a/.env.example b/.env.example
index 690faf4..7e94b2e 100644
--- a/.env.example
+++ b/.env.example
@@ -2,13 +2,25 @@
# Keep this file up-to-date when you add new variables to \`.env\`.
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
-NODE_ENV=
-CI=
-SKIP_ENV_VALIDATION=
+## Next.js ##
+NODE_ENV=development
+CI=false
+SITE_URL=
+SENTRY_AUTH_TOKEN=
+NEXT_PUBLIC_SITE_URL=
+NEXT_PUBLIC_CONVEX_URL=
+NEXT_PUBLIC_PLAUSIBLE_URL=
+NEXT_PUBLIC_SENTRY_DSN=
+NEXT_PUBLIC_SENTRY_URL=
+NEXT_PUBLIC_SENTRY_ORG=
+NEXT_PUBLIC_SENTRY_PROJECT_NAME=
+
+## Convex ##
CONVEX_SELF_HOSTED_URL=
CONVEX_SELF_HOSTED_ADMIN_KEY=
-CONVEX_AUTH_URL=
-SITE_URL=
+SETUP_SCRIPT_RAN=
+# Convex Auth
+AUTH_URL=convex.example.com
USESEND_API_KEY=
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
@@ -16,8 +28,4 @@ AUTH_AUTHENTIK_ISSUER=
AUTH_MICROSOFT_ENTRA_ID_ID=
AUTH_MICROSOFT_ENTRA_ID_SECRET=
AUTH_MICROSOFT_ENTRA_ID_ISSUER=
-AUTH_MICROSOFT_ENTRA_ID_AUTH_UR=
-SENTRY_AUTH_TOKEN=
-SENTRY_DSN=
-SENTRY_ORG=
-SENTRY_PROJECT_NAME=
+AUTH_MICROSOFT_ENTRA_ID_AUTH_URL=
diff --git a/apps/expo/tsconfig.json b/apps/expo/tsconfig.json
index d4fe979..562b919 100644
--- a/apps/expo/tsconfig.json
+++ b/apps/expo/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": ["@acme/tsconfig/base.json"],
+ "extends": ["@gib/tsconfig/base.json"],
"compilerOptions": {
"jsx": "react-native",
"checkJs": false,
diff --git a/apps/next/.gitignore b/apps/next/.gitignore
deleted file mode 100644
index f886745..0000000
--- a/apps/next/.gitignore
+++ /dev/null
@@ -1,36 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-.yarn/install-state.gz
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# env files (can opt-in for commiting if needed)
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
diff --git a/apps/next/README.md b/apps/next/README.md
deleted file mode 100644
index a98bfa8..0000000
--- a/apps/next/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/apps/next/app/favicon.ico b/apps/next/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/apps/next/app/favicon.ico and /dev/null differ
diff --git a/apps/next/app/fonts/GeistMonoVF.woff b/apps/next/app/fonts/GeistMonoVF.woff
deleted file mode 100644
index f2ae185..0000000
Binary files a/apps/next/app/fonts/GeistMonoVF.woff and /dev/null differ
diff --git a/apps/next/app/fonts/GeistVF.woff b/apps/next/app/fonts/GeistVF.woff
deleted file mode 100644
index 1b62daa..0000000
Binary files a/apps/next/app/fonts/GeistVF.woff and /dev/null differ
diff --git a/apps/next/app/globals.css b/apps/next/app/globals.css
deleted file mode 100644
index 6af7ecb..0000000
--- a/apps/next/app/globals.css
+++ /dev/null
@@ -1,50 +0,0 @@
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
-}
-
-body {
- color: var(--foreground);
- background: var(--background);
-}
-
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-.imgDark {
- display: none;
-}
-
-@media (prefers-color-scheme: dark) {
- html {
- color-scheme: dark;
- }
-
- .imgLight {
- display: none;
- }
- .imgDark {
- display: unset;
- }
-}
diff --git a/apps/next/app/layout.tsx b/apps/next/app/layout.tsx
deleted file mode 100644
index 8469537..0000000
--- a/apps/next/app/layout.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { Metadata } from "next";
-import localFont from "next/font/local";
-import "./globals.css";
-
-const geistSans = localFont({
- src: "./fonts/GeistVF.woff",
- variable: "--font-geist-sans",
-});
-const geistMono = localFont({
- src: "./fonts/GeistMonoVF.woff",
- variable: "--font-geist-mono",
-});
-
-export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
-};
-
-export default function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
-
-
- {children}
-
-
- );
-}
diff --git a/apps/next/app/page.module.css b/apps/next/app/page.module.css
deleted file mode 100644
index 3630662..0000000
--- a/apps/next/app/page.module.css
+++ /dev/null
@@ -1,188 +0,0 @@
-.page {
- --gray-rgb: 0, 0, 0;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
-
- --button-primary-hover: #383838;
- --button-secondary-hover: #f2f2f2;
-
- display: grid;
- grid-template-rows: 20px 1fr 20px;
- align-items: center;
- justify-items: center;
- min-height: 100svh;
- padding: 80px;
- gap: 64px;
- font-synthesis: none;
-}
-
-@media (prefers-color-scheme: dark) {
- .page {
- --gray-rgb: 255, 255, 255;
- --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
- --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
-
- --button-primary-hover: #ccc;
- --button-secondary-hover: #1a1a1a;
- }
-}
-
-.main {
- display: flex;
- flex-direction: column;
- gap: 32px;
- grid-row-start: 2;
-}
-
-.main ol {
- font-family: var(--font-geist-mono);
- padding-left: 0;
- margin: 0;
- font-size: 14px;
- line-height: 24px;
- letter-spacing: -0.01em;
- list-style-position: inside;
-}
-
-.main li:not(:last-of-type) {
- margin-bottom: 8px;
-}
-
-.main code {
- font-family: inherit;
- background: var(--gray-alpha-100);
- padding: 2px 4px;
- border-radius: 4px;
- font-weight: 600;
-}
-
-.ctas {
- display: flex;
- gap: 16px;
-}
-
-.ctas a {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- font-family: var(--font-geist-sans);
- border: 1px solid transparent;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
-}
-
-a.primary {
- background: var(--foreground);
- color: var(--background);
- gap: 8px;
-}
-
-a.secondary {
- border-color: var(--gray-alpha-200);
- min-width: 180px;
-}
-
-button.secondary {
- appearance: none;
- border-radius: 128px;
- height: 48px;
- padding: 0 20px;
- border: none;
- font-family: var(--font-geist-sans);
- border: 1px solid transparent;
- transition: background 0.2s, color 0.2s, border-color 0.2s;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 16px;
- line-height: 20px;
- font-weight: 500;
- background: transparent;
- border-color: var(--gray-alpha-200);
- min-width: 180px;
-}
-
-.footer {
- font-family: var(--font-geist-sans);
- grid-row-start: 3;
- display: flex;
- gap: 24px;
-}
-
-.footer a {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.footer img {
- flex-shrink: 0;
-}
-
-/* Enable hover only on non-touch devices */
-@media (hover: hover) and (pointer: fine) {
- a.primary:hover {
- background: var(--button-primary-hover);
- border-color: transparent;
- }
-
- a.secondary:hover {
- background: var(--button-secondary-hover);
- border-color: transparent;
- }
-
- .footer a:hover {
- text-decoration: underline;
- text-underline-offset: 4px;
- }
-}
-
-@media (max-width: 600px) {
- .page {
- padding: 32px;
- padding-bottom: 80px;
- }
-
- .main {
- align-items: center;
- }
-
- .main ol {
- text-align: center;
- }
-
- .ctas {
- flex-direction: column;
- }
-
- .ctas a {
- font-size: 14px;
- height: 40px;
- padding: 0 16px;
- }
-
- a.secondary {
- min-width: auto;
- }
-
- .footer {
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- .logo {
- filter: invert();
- }
-}
diff --git a/apps/next/app/page.tsx b/apps/next/app/page.tsx
deleted file mode 100644
index 1fee7e2..0000000
--- a/apps/next/app/page.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import Image, { type ImageProps } from "next/image";
-import { Button } from "@repo/ui/button";
-import styles from "./page.module.css";
-
-type Props = Omit & {
- srcLight: string;
- srcDark: string;
-};
-
-const ThemeImage = (props: Props) => {
- const { srcLight, srcDark, ...rest } = props;
-
- return (
- <>
-
-
- >
- );
-};
-
-export default function Home() {
- return (
-
-
-
-
- -
- Get started by editing
apps/web/app/page.tsx
-
- - Save and see your changes instantly.
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/next/eslint.config.js b/apps/next/eslint.config.js
deleted file mode 100644
index 47b0670..0000000
--- a/apps/next/eslint.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { nextJsConfig } from "@repo/eslint-config/next-js";
-
-/** @type {import("eslint").Linter.Config[]} */
-export default nextJsConfig;
diff --git a/apps/next/eslint.config.ts b/apps/next/eslint.config.ts
new file mode 100644
index 0000000..e0e2cbe
--- /dev/null
+++ b/apps/next/eslint.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "eslint/config";
+
+import { baseConfig, restrictEnvAccess } from "@gib/eslint-config/base";
+import { nextjsConfig } from "@gib/eslint-config/nextjs";
+import { reactConfig } from "@gib/eslint-config/react";
+
+export default defineConfig(
+ {
+ ignores: [".next/**"],
+ },
+ baseConfig,
+ reactConfig,
+ nextjsConfig,
+ restrictEnvAccess,
+);
+
diff --git a/apps/next/next.config.js b/apps/next/next.config.js
index 4678774..0beb120 100644
--- a/apps/next/next.config.js
+++ b/apps/next/next.config.js
@@ -1,4 +1,65 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {};
+import { env } from './src/env';
+import { withSentryConfig } from '@sentry/nextjs';
+import { withPlausibleProxy } from 'next-plausible';
+import { createJiti } from "jiti";
-export default nextConfig;
+const jiti = createJiti(import.meta.url);
+
+// Import env files to validate at build time. Use jiti so we can load .ts files in here.
+await jiti.import("./src/env");
+
+/** @type {import("next").NextConfig} */
+const config = withPlausibleProxy({
+ customDomain: env.NEXT_PUBLIC_PLAUSIBLE_URL,
+})({
+ output: 'standalone',
+ images: {
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: '*.gbrown.org',
+ },
+ ],
+ },
+ serverExternalPackages: ['require-in-the-middle'],
+ experimental: {
+ serverActions: {
+ bodySizeLimit: '10mb',
+ },
+ },
+ /** Enables hot reloading for local packages without a build step */
+ transpilePackages: [
+ "@gib/backend",
+ "@gib/ui",
+ ],
+ typescript: { ignoreBuildErrors: true },
+});
+
+
+
+const sentryConfig = {
+ // For all available options, see:
+ // https://www.npmjs.com/package/@sentry/webpack-plugin#options
+ org: env.NEXT_PUBLIC_SENTRY_ORG,
+ project: env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
+ sentryUrl: env.NEXT_PUBLIC_SENTRY_URL,
+ authToken: env.SENTRY_AUTH_TOKEN,
+ // Only print logs for uploading source maps in CI
+ silent: !env.CI,
+ // For all available options, see:
+ // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
+ // Upload a larger set of source maps for prettier stack traces (increases build time)
+ widenClientFileUpload: true,
+ // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
+ // This can increase your server load as well as your hosting bill.
+ // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
+ // side errors will fail.
+ tunnelRoute: '/monitoring',
+ // Automatically tree-shake Sentry logger statements to reduce bundle size
+ disableLogger: true,
+ // Capture React Component Names
+ reactComponentAnnotation: {
+ enabled: true,
+ },
+};
+export default withSentryConfig(config, sentryConfig);
diff --git a/apps/next/package.json b/apps/next/package.json
index badd115..fa4d61b 100644
--- a/apps/next/package.json
+++ b/apps/next/package.json
@@ -6,11 +6,11 @@
"scripts": {
"build": "bun with-env next build",
"clean": "git clean -xdf .cache .next .turbo node_modules",
- "dev": "pnpm with-env next dev --turbo",
- "dev:tunnel": "pnpm with-env next dev --turbo",
+ "dev": "bun with-env next dev --turbo",
+ "dev:tunnel": "bun with-env next dev --turbo",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config",
- "start": "pnpm with-env next start",
+ "start": "bun with-env next start",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
diff --git a/apps/next/postcss.config.js b/apps/next/postcss.config.js
new file mode 100644
index 0000000..499d250
--- /dev/null
+++ b/apps/next/postcss.config.js
@@ -0,0 +1 @@
+export { default } from '@gib/tailwind-config/postcss-config';
diff --git a/apps/next/public/favicon.ico b/apps/next/public/favicon.ico
new file mode 100644
index 0000000..f0058b4
Binary files /dev/null and b/apps/next/public/favicon.ico differ
diff --git a/apps/next/public/file-text.svg b/apps/next/public/file-text.svg
deleted file mode 100644
index 9cfb3c9..0000000
--- a/apps/next/public/file-text.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/apps/next/public/globe.svg b/apps/next/public/globe.svg
deleted file mode 100644
index 4230a3d..0000000
--- a/apps/next/public/globe.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/apps/next/public/next.svg b/apps/next/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/apps/next/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/apps/next/public/t3-icon.svg b/apps/next/public/t3-icon.svg
new file mode 100644
index 0000000..e377165
--- /dev/null
+++ b/apps/next/public/t3-icon.svg
@@ -0,0 +1,13 @@
+
diff --git a/apps/next/public/turborepo-dark.svg b/apps/next/public/turborepo-dark.svg
deleted file mode 100644
index dae38fe..0000000
--- a/apps/next/public/turborepo-dark.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/apps/next/public/turborepo-light.svg b/apps/next/public/turborepo-light.svg
deleted file mode 100644
index ddea915..0000000
--- a/apps/next/public/turborepo-light.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
diff --git a/apps/next/public/vercel.svg b/apps/next/public/vercel.svg
deleted file mode 100644
index 0164ddc..0000000
--- a/apps/next/public/vercel.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
diff --git a/apps/next/public/window.svg b/apps/next/public/window.svg
deleted file mode 100644
index bbc7800..0000000
--- a/apps/next/public/window.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/apps/next/src/app/_components/auth-showcase.tsx b/apps/next/src/app/_components/auth-showcase.tsx
new file mode 100644
index 0000000..685a3f2
--- /dev/null
+++ b/apps/next/src/app/_components/auth-showcase.tsx
@@ -0,0 +1,58 @@
+import { headers } from "next/headers";
+import { redirect } from "next/navigation";
+
+import { Button } from "@acme/ui/button";
+
+import { auth, getSession } from "~/auth/server";
+
+export async function AuthShowcase() {
+ const session = await getSession();
+
+ if (!session) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ Logged in as {session.user.name}
+
+
+
+
+ );
+}
diff --git a/apps/next/src/app/_components/posts.tsx b/apps/next/src/app/_components/posts.tsx
new file mode 100644
index 0000000..5a4bbeb
--- /dev/null
+++ b/apps/next/src/app/_components/posts.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import { useForm } from "@tanstack/react-form";
+import {
+ useMutation,
+ useQueryClient,
+ useSuspenseQuery,
+} from "@tanstack/react-query";
+
+import type { RouterOutputs } from "@acme/api";
+import { CreatePostSchema } from "@acme/db/schema";
+import { cn } from "@acme/ui";
+import { Button } from "@acme/ui/button";
+import {
+ Field,
+ FieldContent,
+ FieldError,
+ FieldGroup,
+ FieldLabel,
+} from "@acme/ui/field";
+import { Input } from "@acme/ui/input";
+import { toast } from "@acme/ui/toast";
+
+import { useTRPC } from "~/trpc/react";
+
+export function CreatePostForm() {
+ const trpc = useTRPC();
+
+ const queryClient = useQueryClient();
+ const createPost = useMutation(
+ trpc.post.create.mutationOptions({
+ onSuccess: async () => {
+ form.reset();
+ await queryClient.invalidateQueries(trpc.post.pathFilter());
+ },
+ onError: (err) => {
+ toast.error(
+ err.data?.code === "UNAUTHORIZED"
+ ? "You must be logged in to post"
+ : "Failed to create post",
+ );
+ },
+ }),
+ );
+
+ const form = useForm({
+ defaultValues: {
+ content: "",
+ title: "",
+ },
+ validators: {
+ onSubmit: CreatePostSchema,
+ },
+ onSubmit: (data) => createPost.mutate(data.value),
+ });
+
+ return (
+
+ );
+}
+
+export function PostList() {
+ const trpc = useTRPC();
+ const { data: posts } = useSuspenseQuery(trpc.post.all.queryOptions());
+
+ if (posts.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {posts.map((p) => {
+ return
;
+ })}
+
+ );
+}
+
+export function PostCard(props: {
+ post: RouterOutputs["post"]["all"][number];
+}) {
+ const trpc = useTRPC();
+ const queryClient = useQueryClient();
+ const deletePost = useMutation(
+ trpc.post.delete.mutationOptions({
+ onSuccess: async () => {
+ await queryClient.invalidateQueries(trpc.post.pathFilter());
+ },
+ onError: (err) => {
+ toast.error(
+ err.data?.code === "UNAUTHORIZED"
+ ? "You must be logged in to delete a post"
+ : "Failed to delete post",
+ );
+ },
+ }),
+ );
+
+ return (
+
+
+
{props.post.title}
+
{props.post.content}
+
+
+
+
+
+ );
+}
+
+export function PostCardSkeleton(props: { pulse?: boolean }) {
+ const { pulse = true } = props;
+ return (
+
+ );
+}
diff --git a/apps/next/src/app/api/auth/[...all]/route.ts b/apps/next/src/app/api/auth/[...all]/route.ts
new file mode 100644
index 0000000..71c7ff7
--- /dev/null
+++ b/apps/next/src/app/api/auth/[...all]/route.ts
@@ -0,0 +1,4 @@
+import { auth } from "~/auth/server";
+
+export const GET = auth.handler;
+export const POST = auth.handler;
diff --git a/apps/next/src/app/api/trpc/[trpc]/route.ts b/apps/next/src/app/api/trpc/[trpc]/route.ts
new file mode 100644
index 0000000..c5548f9
--- /dev/null
+++ b/apps/next/src/app/api/trpc/[trpc]/route.ts
@@ -0,0 +1,46 @@
+import type { NextRequest } from "next/server";
+import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
+
+import { appRouter, createTRPCContext } from "@acme/api";
+
+import { auth } from "~/auth/server";
+
+/**
+ * Configure basic CORS headers
+ * You should extend this to match your needs
+ */
+const setCorsHeaders = (res: Response) => {
+ res.headers.set("Access-Control-Allow-Origin", "*");
+ res.headers.set("Access-Control-Request-Method", "*");
+ res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
+ res.headers.set("Access-Control-Allow-Headers", "*");
+};
+
+export const OPTIONS = () => {
+ const response = new Response(null, {
+ status: 204,
+ });
+ setCorsHeaders(response);
+ return response;
+};
+
+const handler = async (req: NextRequest) => {
+ const response = await fetchRequestHandler({
+ endpoint: "/api/trpc",
+ router: appRouter,
+ req,
+ createContext: () =>
+ createTRPCContext({
+ auth: auth,
+ headers: req.headers,
+ }),
+ onError({ error, path }) {
+ console.error(`>>> tRPC Error on '${path}'`, error);
+ },
+ });
+
+ setCorsHeaders(response);
+ return response;
+};
+
+export { handler as GET, handler as POST };
diff --git a/apps/next/src/app/layout.tsx b/apps/next/src/app/layout.tsx
new file mode 100644
index 0000000..8985f5d
--- /dev/null
+++ b/apps/next/src/app/layout.tsx
@@ -0,0 +1,70 @@
+import type { Metadata, Viewport } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+
+import { cn } from "@acme/ui";
+import { ThemeProvider, ThemeToggle } from "@acme/ui/theme";
+import { Toaster } from "@acme/ui/toast";
+
+import { env } from "~/env";
+import { TRPCReactProvider } from "~/trpc/react";
+
+import "~/app/styles.css";
+
+export const metadata: Metadata = {
+ metadataBase: new URL(
+ env.VERCEL_ENV === "production"
+ ? "https://turbo.t3.gg"
+ : "http://localhost:3000",
+ ),
+ title: "Create T3 Turbo",
+ description: "Simple monorepo with shared backend for web & mobile apps",
+ openGraph: {
+ title: "Create T3 Turbo",
+ description: "Simple monorepo with shared backend for web & mobile apps",
+ url: "https://create-t3-turbo.vercel.app",
+ siteName: "Create T3 Turbo",
+ },
+ twitter: {
+ card: "summary_large_image",
+ site: "@jullerino",
+ creator: "@jullerino",
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+const geistSans = Geist({
+ subsets: ["latin"],
+ variable: "--font-geist-sans",
+});
+const geistMono = Geist_Mono({
+ subsets: ["latin"],
+ variable: "--font-geist-mono",
+});
+
+export default function RootLayout(props: { children: React.ReactNode }) {
+ return (
+
+
+
+ {props.children}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/next/src/app/page.tsx b/apps/next/src/app/page.tsx
new file mode 100644
index 0000000..28a5d24
--- /dev/null
+++ b/apps/next/src/app/page.tsx
@@ -0,0 +1,41 @@
+import { Suspense } from "react";
+
+import { HydrateClient, prefetch, trpc } from "~/trpc/server";
+import { AuthShowcase } from "./_components/auth-showcase";
+import {
+ CreatePostForm,
+ PostCardSkeleton,
+ PostList,
+} from "./_components/posts";
+
+export default function HomePage() {
+ prefetch(trpc.post.all.queryOptions());
+
+ return (
+
+
+
+
+ Create T3 Turbo
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/apps/next/src/app/styles.css b/apps/next/src/app/styles.css
new file mode 100644
index 0000000..25d002f
--- /dev/null
+++ b/apps/next/src/app/styles.css
@@ -0,0 +1,29 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+@import "@acme/tailwind-config/theme";
+
+@source "../../../../packages/ui/src/*.{ts,tsx}";
+
+@custom-variant dark (&:where(.dark, .dark *));
+@custom-variant light (&:where(.light, .light *));
+@custom-variant auto (&:where(.auto, .auto *));
+
+@utility container {
+ margin-inline: auto;
+ padding-inline: 2rem;
+ @media (width >= --theme(--breakpoint-sm)) {
+ max-width: none;
+ }
+ @media (width >= 1400px) {
+ max-width: 1400px;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ letter-spacing: var(--tracking-normal);
+ }
+}
diff --git a/apps/next/src/env.ts b/apps/next/src/env.ts
new file mode 100644
index 0000000..67dfede
--- /dev/null
+++ b/apps/next/src/env.ts
@@ -0,0 +1,48 @@
+import { createEnv } from "@t3-oss/env-nextjs";
+import { z } from "zod/v4";
+
+export const env = createEnv({
+ shared: {
+ NODE_ENV: z
+ .enum(["development", "production", "test"])
+ .default("development"),
+ CI: z.boolean().default(false),
+ SITE_URL: z.string().default("http://localhost:3000"),
+ },
+ /**
+ * Specify your server-side environment variables schema here.
+ * This way you can ensure the app isn't built with invalid env vars.
+ */
+ server: {
+ SENTRY_AUTH_TOKEN: z.string(),
+ },
+
+ /**
+ * Specify your client-side environment variables schema here.
+ * For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
+ */
+ client: {
+ NEXT_PUBLIC_CONVEX_URL: z.url(),
+ NEXT_PUBLIC_PLAUSIBLE_URL: z.url(),
+ NEXT_PUBLIC_SENTRY_DSN: z.string(),
+ NEXT_PUBLIC_SENTRY_URL: z.string(),
+ NEXT_PUBLIC_SENTRY_ORG: z.string(),
+ NEXT_PUBLIC_SENTRY_PROJECT_NAME: z.string(),
+ },
+ /**
+ * Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
+ */
+ experimental__runtimeEnv: {
+ NODE_ENV: process.env.NODE_ENV,
+ CI: process.env.CI,
+ SITE_URL: process.env.SITE_URL,
+ NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL,
+ NEXT_PUBLIC_PLAUSIBLE_URL: process.env.NEXT_PUBLIC_PLAUSIBLE_URL,
+ NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
+ NEXT_PUBLIC_SENTRY_URL: process.env.NEXT_PUBLIC_SENTRY_URL,
+ NEXT_PUBLIC_SENTRY_ORG: process.env.NEXT_PUBLIC_SENTRY_ORG,
+ NEXT_PUBLIC_SENTRY_PROJECT_NAME: process.env.NEXT_PUBLIC_SENTRY_PROJECT_NAME,
+ },
+ skipValidation:
+ !!process.env.CI || process.env.npm_lifecycle_event === "lint",
+});
diff --git a/apps/next/tsconfig.json b/apps/next/tsconfig.json
index 7aef056..ad25c8b 100644
--- a/apps/next/tsconfig.json
+++ b/apps/next/tsconfig.json
@@ -1,20 +1,13 @@
{
- "extends": "@repo/typescript-config/nextjs.json",
+ "extends": "@gib/tsconfig/base.json",
"compilerOptions": {
- "plugins": [
- {
- "name": "next"
- }
- ]
+ "lib": ["ES2022", "dom", "dom.iterable"],
+ "jsx": "preserve",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "plugins": [{ "name": "next" }]
},
- "include": [
- "**/*.ts",
- "**/*.tsx",
- "next-env.d.ts",
- "next.config.js",
- ".next/types/**/*.ts"
- ],
- "exclude": [
- "node_modules"
- ]
+ "include": [".", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
}
diff --git a/bun.lock b/bun.lock
index d9aa4aa..70598ab 100644
--- a/bun.lock
+++ b/bun.lock
@@ -105,7 +105,6 @@
"@oslojs/crypto": "^1.0.1",
"@react-email/components": "0.5.4",
"@react-email/render": "^1.4.0",
- "@t3-oss/env-core": "^0.13.8",
"convex": "catalog:convex",
"react": "catalog:react19",
"react-dom": "catalog:react19",
diff --git a/docker/.dockerignore b/docker/.dockerignore
new file mode 100644
index 0000000..b5b7a86
--- /dev/null
+++ b/docker/.dockerignore
@@ -0,0 +1,38 @@
+# Dependencies
+node_modules
+bun.lockb
+*.lock
+
+# Next.js
+apps/next/.next
+apps/next/out
+apps/next/.turbo
+
+# Development
+.git
+.gitignore
+*.log
+*.md
+README.md
+.env*.local
+.vscode
+.idea
+
+# Tests
+**/__tests__
+**/*.test.ts
+**/*.test.tsx
+**/*.spec.ts
+
+# Build artifacts
+dist
+build
+.turbo
+
+# Convex local
+packages/backend/.convex
+convex/_generated
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/docker/.env.example b/docker/.env.example
new file mode 100644
index 0000000..819cc41
--- /dev/null
+++ b/docker/.env.example
@@ -0,0 +1,35 @@
+# Next Envrionment Variables
+NETWORK=nginx-bridge
+NEXT_CONTAINER_NAME=next-app
+NEXT_DOMAIN_NAME=gbrown.org
+# Port is disabled by default as suggested
+# config is to have reverse proxy on the same
+# network so you can just forward to the
+# port on the internal network.
+# NEXT_PORT=3000
+
+# Convex Environment Variables
+BACKEND_TAG=latest
+BACKEND_CONTAINER_NAME=convex-backend
+BACKEND_DOMAIN_NAME=convex.gbrown.org
+#BACKEND_PORT=
+#SITE_PROXY_PORT=
+DASHBOARD_TAG=latest
+DASHBOARD_CONTAINER_NAME=convex-dashboard
+DASHBOARD_DOMAIN=dashboard.convex.gbrown.org
+#DASHBOARD_PORT
+INSTANCE_NAME=convex
+#INSTANCE_SECRET=
+CONVEX_CLOUD_ORIGIN=https://api.convex.gbrown.org
+CONVEX_SITE_ORIGIN=https://convex.gbrown.org
+DISABLE_BEACON=true
+REDACT_LOGS_TO_CLIENT=true
+DO_NOT_REQUIRE_SSL=true
+NEXT_PUBLIC_DEPLOYMENT_URL=https://api.convex.gbrown.org
+#POSTGRES_URL=
+#DATABASE_URL=
+#CONVEX_RELEASE_VERSION_DEV=
+#ACTIONS_USER_TIMEOUT_SECS=
+#MYSQL_URL=
+#RUST_LOG=
+#RUST_BACKTRACE=
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..ea818bd
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,57 @@
+# syntax=docker/dockerfile:1
+FROM oven/bun:alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Copy root package files
+COPY package.json bun.lockb turbo.json ./
+COPY apps/next/package.json ./apps/next/package.json
+COPY packages/backend/package.json ./packages/backend/package.json
+COPY tools/*/package.json ./tools/
+
+# Install dependencies
+RUN bun install --frozen-lockfile
+
+# Builder stage
+FROM base AS builder
+WORKDIR /app
+
+# Copy dependencies
+COPY --from=deps /app/node_modules ./node_modules
+COPY --from=deps /app/apps/next/node_modules ./apps/next/node_modules
+COPY --from=deps /app/packages/backend/node_modules ./packages/backend/node_modules
+
+# Copy source code
+COPY . .
+
+# Build
+ENV NEXT_TELEMETRY_DISABLED=1
+ENV SKIP_ENV_VALIDATION=1
+RUN bun run build --filter=@gib/next
+
+# Runner stage
+FROM node:20-alpine AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN addgroup --system --gid 1001 nodejs && \
+ adduser --system --uid 1001 nextjs
+
+# Copy built application
+COPY --from=builder --chown=nextjs:nodejs /app/apps/next/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/apps/next/.next/static ./apps/next/.next/static
+COPY --from=builder --chown=nextjs:nodejs /app/apps/next/public ./apps/next/public
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+
+CMD ["node", "apps/next/server.js"]
diff --git a/docker/compose.yml b/docker/compose.yml
new file mode 100644
index 0000000..5bcfecd
--- /dev/null
+++ b/docker/compose.yml
@@ -0,0 +1,71 @@
+networks:
+ nginx-bridge: # You need to change this line to your defined network is as well
+ external: true
+
+services:
+ next-app:
+ build:
+ context: ../
+ dockerfile: ./docker/Dockerfile
+ image: ${NEXT_CONTAINER_NAME}:alpine
+ container_name: ${NEXT_CONTAINER_NAME}
+ env_file: [.env]
+ hostname: ${NEXT_CONTAINER_NAME}
+ domainname: ${NEXT_DOMAIN_NAME}
+ networks: ['${NETWORK:-nginx-bridge}']
+ #ports: ['${NEXT_PORT}:3000']
+ depends_on: ['convex-backend']
+ tty: true
+ stdin_open: true
+ restart: unless-stopped
+
+ convex-backend:
+ image: ghcr.io/get-convex/convex-backend:${BACKEND_TAG:-latest}
+ container_name: ${BACKEND_CONTAINER_NAME:-convex-backend}
+ hostname: ${BACKEND_CONTAINER_NAME:-convex-backend}
+ domainname: ${BACKEND_DOMAIN_NAME:-convex.gbrown.org}
+ networks: ['${NETWORK:-nginx-bridge}']
+ #user: '1000:1000'
+ #ports: ['${BACKEND_PORT:-3210}:3210','${SITE_PROXY_PORT:-3211}:3211']
+ volumes: [./data:/convex/data]
+ labels: ['com.centurylinklabs.watchtower.enable=true']
+ env_file: ['.env']
+ environment:
+ - INSTANCE_NAME
+ - INSTANCE_SECRET
+ - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${BACKEND_PORT:-3210}}
+ - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${SITE_PROXY_PORT:-3211}}
+ - DISABLE_BEACON
+ - REDACT_LOGS_TO_CLIENT
+ - DO_NOT_REQUIRE_SSL
+ #- DATABASE_URL=${DATABASE_URL:-}
+ stdin_open: true
+ tty: true
+ restart: unless-stopped
+ healthcheck:
+ test: curl -f http://localhost:3210/version
+ interval: 5s
+ start_period: 10s
+ stop_grace_period: 10s
+ stop_signal: SIGINT
+
+ convex-dashboard:
+ image: ghcr.io/get-convex/convex-dashboard:${DASHBOARD_TAG:-latest}
+ container_name: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard}
+ hostname: ${DASHBOARD_CONTAINER_NAME:-convex-dashboard}
+ domainname: ${DASHBOARD_DOMAIN_NAME:-dashboard.${BACKEND_DOMAIN_NAME:-convex.gbrown.org}}
+ networks: ['${NETWORK:-nginx-bridge}']
+ #user: 1000:1000
+ #ports: ['${DASHBOARD_PORT:-6791}:6791']
+ labels: ['com.centurylinklabs.watchtower.enable=true']
+ env_file: [.env]
+ environment:
+ - NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://${BACKEND_CONTAINER_NAME:-convex-backend}:${PORT:-3210}}
+ depends_on:
+ convex-backend:
+ condition: service_healthy
+ stdin_open: true
+ tty: true
+ restart: unless-stopped
+ stop_grace_period: 10s
+ stop_signal: SIGINT
diff --git a/package.json b/package.json
index 94622d7..6fc370f 100644
--- a/package.json
+++ b/package.json
@@ -38,9 +38,10 @@
"clean:ws": "turbo run clean",
"dev": "turbo run dev",
"dev:tunnel": "turbo run dev:tunnel",
- "dev:next": "turbo run dev -F @gib/next",
- "dev:expo": "turbo run dev -F @gib/expo",
- "dev:expo:tunnel": "turbo run dev -F @gib/expo --tunnel",
+ "dev:next": "turbo run dev -F @gib/next -F @gib/backend",
+ "dev:expo": "turbo run dev -F @gib/expo -F @gib/backend",
+ "dev:backend": "turbo run dev -F @gib/backend",
+ "dev:expo:tunnel": "turbo run dev:tunnel -F @gib/expo -F @gib/backend",
"format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache",
"format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache",
"lint": "turbo run lint --continue -- --cache --cache-location .cache/.eslintcache",
diff --git a/packages/backend/env.ts b/packages/backend/env.ts
deleted file mode 100644
index 2e6e1e4..0000000
--- a/packages/backend/env.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createEnv } from "@t3-oss/env-core";
-import { z } from "zod/v4";
-
-export const convexEnv = () => {
- return createEnv({
- server: {},
- runtimeEnv: process.env,
- skipValidation: !!process.env.CI,
- });
-};
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 3f140e2..6cca95d 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -20,7 +20,6 @@
"@oslojs/crypto": "^1.0.1",
"@react-email/components": "0.5.4",
"@react-email/render": "^1.4.0",
- "@t3-oss/env-core": "^0.13.8",
"convex": "catalog:convex",
"react": "catalog:react19",
"react-dom": "catalog:react19",
diff --git a/tools/eslint/base.ts b/tools/eslint/base.ts
index e126739..30354b1 100644
--- a/tools/eslint/base.ts
+++ b/tools/eslint/base.ts
@@ -20,7 +20,7 @@ export const restrictEnvAccess = defineConfig(
object: "process",
property: "env",
message:
- "Use `import { env } from '~/env'` instead to ensure validated types.",
+ "Use `import { env } from '@/env'` instead to ensure validated types.",
},
],
"no-restricted-imports": [
@@ -29,7 +29,7 @@ export const restrictEnvAccess = defineConfig(
name: "process",
importNames: ["env"],
message:
- "Use `import { env } from '~/env'` instead to ensure validated types.",
+ "Use `import { env } from '@/env'` instead to ensure validated types.",
},
],
},
diff --git a/turbo.json b/turbo.json
index 2a92398..c954103 100644
--- a/turbo.json
+++ b/turbo.json
@@ -2,12 +2,30 @@
"$schema": "https://turborepo.com/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": [
- "CONVEX_SELF_HOSTED_URL",
- "CONVEX_SELF_HOSTED_ADMIN_KEY",
- "CONVEX_AUTH_URL",
+ "NODE_ENV",
+ "CI",
+ "SKIP_ENV_VALIDATION",
"SITE_URL",
"SENTRY_AUTH_TOKEN",
- "SENTRY_ORG"
+ "NEXT_PUBLIC_SITE_URL",
+ "NEXT_PUBLIC_CONVEX_URL",
+ "NEXT_PUBLIC_SENTRY_DSN",
+ "NEXT_PUBLIC_SENTRY_URL",
+ "NEXT_PUBLIC_SENTRY_ORG",
+ "NEXT_PUBLIC_SENTRY_PROJECT_NAME",
+ "CONVEX_SELF_HOSTED_URL",
+ "CONVEX_SELF_HOSTED_ADMIN_KEY",
+ "SETUP_SCRIPT_RAN",
+ "SITE_URL",
+ "AUTH_URL",
+ "USESEND_API_KEY",
+ "AUTH_AUTHENTIK_ID",
+ "AUTH_AUTHENTIK_SECRET",
+ "AUTH_AUTHENTIK_ISSUER",
+ "AUTH_MICROSOFT_ENTRA_ID_ID",
+ "AUTH_MICROSOFT_ENTRA_ID_SECRET",
+ "AUTH_MICROSOFT_ENTRA_ID_ISSUER",
+ "AUTH_MICROSOFT_ENTRA_ID_AUTH_URL"
],
"globalPassThroughEnv": [
"NODE_ENV",