create-n8-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +239 -0
  5. package/package.json +58 -0
  6. package/template/_env.example +34 -0
  7. package/template/_env.local +31 -0
  8. package/template/_eslintrc.json +3 -0
  9. package/template/_gitignore +46 -0
  10. package/template/_package.json +59 -0
  11. package/template/app/api/auth/[...nextauth]/route.ts +3 -0
  12. package/template/app/api/trpc/[trpc]/route.ts +19 -0
  13. package/template/app/auth/signin/page.tsx +39 -0
  14. package/template/app/globals.css +33 -0
  15. package/template/app/layout.tsx +28 -0
  16. package/template/app/page.tsx +68 -0
  17. package/template/app/providers.tsx +47 -0
  18. package/template/components/auth/user-button.tsx +46 -0
  19. package/template/components/ui/.gitkeep +2 -0
  20. package/template/components.json +21 -0
  21. package/template/drizzle/.gitkeep +1 -0
  22. package/template/drizzle.config.ts +10 -0
  23. package/template/hooks/.gitkeep +1 -0
  24. package/template/lib/auth.ts +44 -0
  25. package/template/lib/db.ts +10 -0
  26. package/template/lib/env.ts +76 -0
  27. package/template/lib/trpc.ts +4 -0
  28. package/template/lib/utils.ts +6 -0
  29. package/template/next-env.d.ts +5 -0
  30. package/template/next.config.ts +14 -0
  31. package/template/postcss.config.mjs +7 -0
  32. package/template/prettier.config.js +11 -0
  33. package/template/public/.gitkeep +1 -0
  34. package/template/server/api/root.ts +22 -0
  35. package/template/server/api/routers/example.ts +76 -0
  36. package/template/server/api/trpc.ts +67 -0
  37. package/template/server/db/schema.ts +95 -0
  38. package/template/stores/example-store.ts +121 -0
  39. package/template/tests/example.test.ts +22 -0
  40. package/template/tests/setup.ts +22 -0
  41. package/template/tsconfig.json +27 -0
  42. package/template/vitest.config.ts +30 -0
@@ -0,0 +1,39 @@
1
+ import { signIn } from '@/lib/auth'
2
+
3
+ export default function SignInPage() {
4
+ return (
5
+ <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-slate-900 to-slate-800">
6
+ <div className="flex flex-col items-center gap-8 rounded-xl bg-white/10 p-8">
7
+ <h1 className="text-3xl font-bold text-white">Sign In</h1>
8
+ <p className="text-slate-300">Sign in to access your account</p>
9
+
10
+ <form
11
+ action={async () => {
12
+ 'use server'
13
+ await signIn('github', { redirectTo: '/' })
14
+ }}
15
+ >
16
+ <button
17
+ type="submit"
18
+ className="flex items-center gap-3 rounded-lg bg-slate-800 px-6 py-3 text-white transition-colors hover:bg-slate-700"
19
+ >
20
+ <GitHubIcon />
21
+ Sign in with GitHub
22
+ </button>
23
+ </form>
24
+ </div>
25
+ </main>
26
+ )
27
+ }
28
+
29
+ function GitHubIcon() {
30
+ return (
31
+ <svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
32
+ <path
33
+ fillRule="evenodd"
34
+ d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
35
+ clipRule="evenodd"
36
+ />
37
+ </svg>
38
+ )
39
+ }
@@ -0,0 +1,33 @@
1
+ @import 'tailwindcss';
2
+
3
+ @theme {
4
+ /* Custom theme configuration */
5
+ --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
6
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
7
+
8
+ /* Primary colors */
9
+ --color-primary-50: oklch(0.97 0.02 250);
10
+ --color-primary-100: oklch(0.93 0.04 250);
11
+ --color-primary-200: oklch(0.86 0.08 250);
12
+ --color-primary-300: oklch(0.76 0.12 250);
13
+ --color-primary-400: oklch(0.64 0.16 250);
14
+ --color-primary-500: oklch(0.55 0.18 250);
15
+ --color-primary-600: oklch(0.48 0.18 250);
16
+ --color-primary-700: oklch(0.42 0.16 250);
17
+ --color-primary-800: oklch(0.36 0.14 250);
18
+ --color-primary-900: oklch(0.30 0.10 250);
19
+ --color-primary-950: oklch(0.22 0.08 250);
20
+ }
21
+
22
+ @layer base {
23
+ * {
24
+ @apply border-border;
25
+ }
26
+
27
+ body {
28
+ @apply bg-background text-foreground;
29
+ font-feature-settings:
30
+ 'rlig' 1,
31
+ 'calt' 1;
32
+ }
33
+ }
@@ -0,0 +1,28 @@
1
+ import type { Metadata } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import './globals.css'
4
+ import { Providers } from './providers'
5
+
6
+ const inter = Inter({
7
+ subsets: ['latin'],
8
+ variable: '--font-sans',
9
+ })
10
+
11
+ export const metadata: Metadata = {
12
+ title: 'N8 App',
13
+ description: 'Built with the N8 stack - Next.js, Tailwind, Shadcn/ui, Drizzle, tRPC, and more',
14
+ }
15
+
16
+ export default function RootLayout({
17
+ children,
18
+ }: Readonly<{
19
+ children: React.ReactNode
20
+ }>) {
21
+ return (
22
+ <html lang="en" suppressHydrationWarning>
23
+ <body className={`${inter.variable} font-sans antialiased`}>
24
+ <Providers>{children}</Providers>
25
+ </body>
26
+ </html>
27
+ )
28
+ }
@@ -0,0 +1,68 @@
1
+ import Link from 'next/link'
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-slate-900 to-slate-800 text-white">
6
+ <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16">
7
+ <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
8
+ N8 <span className="text-primary-400">Stack</span>
9
+ </h1>
10
+
11
+ <p className="max-w-2xl text-center text-lg text-slate-300">
12
+ A modern, full-stack Next.js template with TypeScript, Tailwind CSS, Shadcn/ui, Drizzle
13
+ ORM, tRPC, TanStack Query, Zustand, and NextAuth.js.
14
+ </p>
15
+
16
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
17
+ <FeatureCard
18
+ title="Next.js 16"
19
+ description="App Router, Server Components, Server Actions, Turbopack"
20
+ href="https://nextjs.org/docs"
21
+ />
22
+ <FeatureCard
23
+ title="Tailwind + Shadcn"
24
+ description="Modern styling with beautiful, accessible components"
25
+ href="https://ui.shadcn.com"
26
+ />
27
+ <FeatureCard
28
+ title="Drizzle + Neon"
29
+ description="Type-safe ORM with serverless Postgres"
30
+ href="https://orm.drizzle.team"
31
+ />
32
+ <FeatureCard
33
+ title="tRPC"
34
+ description="End-to-end type safety for your API"
35
+ href="https://trpc.io"
36
+ />
37
+ </div>
38
+
39
+ <div className="flex flex-col items-center gap-2">
40
+ <p className="text-sm text-slate-400">Get started by editing</p>
41
+ <code className="rounded bg-slate-700 px-3 py-1 font-mono text-sm">app/page.tsx</code>
42
+ </div>
43
+ </div>
44
+ </main>
45
+ )
46
+ }
47
+
48
+ function FeatureCard({
49
+ title,
50
+ description,
51
+ href,
52
+ }: {
53
+ title: string
54
+ description: string
55
+ href: string
56
+ }) {
57
+ return (
58
+ <Link
59
+ className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20 transition-colors"
60
+ href={href}
61
+ target="_blank"
62
+ rel="noopener noreferrer"
63
+ >
64
+ <h3 className="text-2xl font-bold">{title} →</h3>
65
+ <p className="text-slate-300">{description}</p>
66
+ </Link>
67
+ )
68
+ }
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import { httpBatchLink } from '@trpc/client'
5
+ import { SessionProvider } from 'next-auth/react'
6
+ import { useState } from 'react'
7
+ import { trpc } from '@/lib/trpc'
8
+ import superjson from 'superjson'
9
+
10
+ function getBaseUrl() {
11
+ if (typeof window !== 'undefined') return ''
12
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
13
+ return `http://localhost:${process.env.PORT ?? 3000}`
14
+ }
15
+
16
+ export function Providers({ children }: { children: React.ReactNode }) {
17
+ const [queryClient] = useState(
18
+ () =>
19
+ new QueryClient({
20
+ defaultOptions: {
21
+ queries: {
22
+ staleTime: 5 * 1000,
23
+ refetchOnWindowFocus: false,
24
+ },
25
+ },
26
+ })
27
+ )
28
+
29
+ const [trpcClient] = useState(() =>
30
+ trpc.createClient({
31
+ links: [
32
+ httpBatchLink({
33
+ url: `${getBaseUrl()}/api/trpc`,
34
+ transformer: superjson,
35
+ }),
36
+ ],
37
+ })
38
+ )
39
+
40
+ return (
41
+ <SessionProvider>
42
+ <trpc.Provider client={trpcClient} queryClient={queryClient}>
43
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
44
+ </trpc.Provider>
45
+ </SessionProvider>
46
+ )
47
+ }
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import { useSession, signIn, signOut } from 'next-auth/react'
4
+ import Image from 'next/image'
5
+
6
+ export function UserButton() {
7
+ const { data: session, status } = useSession()
8
+
9
+ if (status === 'loading') {
10
+ return (
11
+ <div className="h-8 w-8 animate-pulse rounded-full bg-slate-600" />
12
+ )
13
+ }
14
+
15
+ if (!session) {
16
+ return (
17
+ <button
18
+ onClick={() => signIn('github')}
19
+ className="rounded-lg bg-white/10 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20"
20
+ >
21
+ Sign In
22
+ </button>
23
+ )
24
+ }
25
+
26
+ return (
27
+ <div className="flex items-center gap-3">
28
+ {session.user.image && (
29
+ <Image
30
+ src={session.user.image}
31
+ alt={session.user.name ?? 'User'}
32
+ width={32}
33
+ height={32}
34
+ className="rounded-full"
35
+ />
36
+ )}
37
+ <span className="text-sm text-white">{session.user.name}</span>
38
+ <button
39
+ onClick={() => signOut()}
40
+ className="rounded-lg bg-white/10 px-3 py-1 text-sm text-white transition-colors hover:bg-white/20"
41
+ >
42
+ Sign Out
43
+ </button>
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,2 @@
1
+ # Shadcn/ui components will be added here
2
+ # Run: npx shadcn@latest add [component]
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "slate",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
@@ -0,0 +1 @@
1
+ # Drizzle migrations will be generated here
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+
3
+ export default defineConfig({
4
+ schema: './server/db/schema.ts',
5
+ out: './drizzle',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ })
@@ -0,0 +1 @@
1
+ # Custom React hooks go here
@@ -0,0 +1,44 @@
1
+ import NextAuth from 'next-auth'
2
+ import GitHub from 'next-auth/providers/github'
3
+ import { DrizzleAdapter } from '@auth/drizzle-adapter'
4
+ import { db } from '@/lib/db'
5
+ import { accounts, sessions, users, verificationTokens } from '@/server/db/schema'
6
+
7
+ export const { handlers, signIn, signOut, auth } = NextAuth({
8
+ adapter: DrizzleAdapter(db, {
9
+ usersTable: users,
10
+ accountsTable: accounts,
11
+ sessionsTable: sessions,
12
+ verificationTokensTable: verificationTokens,
13
+ }),
14
+ providers: [
15
+ GitHub({
16
+ clientId: process.env.AUTH_GITHUB_ID,
17
+ clientSecret: process.env.AUTH_GITHUB_SECRET,
18
+ }),
19
+ ],
20
+ callbacks: {
21
+ session: ({ session, user }) => ({
22
+ ...session,
23
+ user: {
24
+ ...session.user,
25
+ id: user.id,
26
+ },
27
+ }),
28
+ },
29
+ pages: {
30
+ signIn: '/auth/signin',
31
+ },
32
+ })
33
+
34
+ // Extend the built-in session types
35
+ declare module 'next-auth' {
36
+ interface Session {
37
+ user: {
38
+ id: string
39
+ name?: string | null
40
+ email?: string | null
41
+ image?: string | null
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,10 @@
1
+ import { neon } from '@neondatabase/serverless'
2
+ import { drizzle } from 'drizzle-orm/neon-http'
3
+ import * as schema from '@/server/db/schema'
4
+ import { env } from '@/lib/env'
5
+
6
+ const sql = neon(env.DATABASE_URL)
7
+
8
+ export const db = drizzle(sql, { schema })
9
+
10
+ export type Database = typeof db
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Server-side environment variables schema
5
+ * These are validated at build time and runtime
6
+ */
7
+ const serverEnvSchema = z.object({
8
+ // Database
9
+ DATABASE_URL: z.string().url().describe('Neon PostgreSQL connection string'),
10
+
11
+ // Authentication
12
+ AUTH_SECRET: z
13
+ .string()
14
+ .min(32)
15
+ .describe('NextAuth.js secret - generate with: openssl rand -base64 32'),
16
+ AUTH_GITHUB_ID: z.string().min(1).describe('GitHub OAuth App Client ID'),
17
+ AUTH_GITHUB_SECRET: z.string().min(1).describe('GitHub OAuth App Client Secret'),
18
+
19
+ // Optional: AI
20
+ OPENAI_API_KEY: z.string().optional().describe('OpenAI API key for AI features'),
21
+
22
+ // Node environment
23
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
24
+ })
25
+
26
+ /**
27
+ * Client-side environment variables schema
28
+ * These are exposed to the browser (prefixed with NEXT_PUBLIC_)
29
+ */
30
+ const clientEnvSchema = z.object({
31
+ NEXT_PUBLIC_APP_URL: z.string().url().default('http://localhost:3000'),
32
+ })
33
+
34
+ /**
35
+ * Validate and export environment variables
36
+ */
37
+ function validateEnv() {
38
+ // Skip validation during build if env vars aren't available
39
+ if (process.env.SKIP_ENV_VALIDATION === 'true') {
40
+ console.warn('⚠️ Skipping environment validation')
41
+ return {
42
+ DATABASE_URL: process.env.DATABASE_URL ?? '',
43
+ AUTH_SECRET: process.env.AUTH_SECRET ?? '',
44
+ AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID ?? '',
45
+ AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET ?? '',
46
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
47
+ NODE_ENV: (process.env.NODE_ENV as 'development' | 'production' | 'test') ?? 'development',
48
+ NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
49
+ }
50
+ }
51
+
52
+ const serverEnv = serverEnvSchema.safeParse(process.env)
53
+ const clientEnv = clientEnvSchema.safeParse(process.env)
54
+
55
+ if (!serverEnv.success) {
56
+ console.error('❌ Invalid server environment variables:')
57
+ console.error(serverEnv.error.flatten().fieldErrors)
58
+ throw new Error('Invalid server environment variables')
59
+ }
60
+
61
+ if (!clientEnv.success) {
62
+ console.error('❌ Invalid client environment variables:')
63
+ console.error(clientEnv.error.flatten().fieldErrors)
64
+ throw new Error('Invalid client environment variables')
65
+ }
66
+
67
+ return {
68
+ ...serverEnv.data,
69
+ ...clientEnv.data,
70
+ }
71
+ }
72
+
73
+ export const env = validateEnv()
74
+
75
+ // Type for the validated environment
76
+ export type Env = typeof env
@@ -0,0 +1,4 @@
1
+ import { createTRPCReact } from '@trpc/react-query'
2
+ import type { AppRouter } from '@/server/api/root'
3
+
4
+ export const trpc = createTRPCReact<AppRouter>()
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
@@ -0,0 +1,14 @@
1
+ import type { NextConfig } from 'next'
2
+
3
+ const nextConfig: NextConfig = {
4
+ // Enable React strict mode for better development experience
5
+ reactStrictMode: true,
6
+
7
+ // Experimental features
8
+ experimental: {
9
+ // Enable Partial Prerendering (PPR) for improved performance
10
+ // ppr: true,
11
+ },
12
+ }
13
+
14
+ export default nextConfig
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
6
+
7
+ export default config
@@ -0,0 +1,11 @@
1
+ /** @type {import('prettier').Config} */
2
+ const config = {
3
+ semi: false,
4
+ singleQuote: true,
5
+ tabWidth: 2,
6
+ trailingComma: 'es5',
7
+ printWidth: 100,
8
+ plugins: ['prettier-plugin-tailwindcss'],
9
+ }
10
+
11
+ export default config
@@ -0,0 +1 @@
1
+ # Static assets go here
@@ -0,0 +1,22 @@
1
+ import { createCallerFactory, createTRPCRouter } from './trpc'
2
+ import { exampleRouter } from './routers/example'
3
+
4
+ /**
5
+ * This is the primary router for your server.
6
+ *
7
+ * All routers added in /api/routers should be manually added here.
8
+ */
9
+ export const appRouter = createTRPCRouter({
10
+ example: exampleRouter,
11
+ })
12
+
13
+ // Export type definition of API
14
+ export type AppRouter = typeof appRouter
15
+
16
+ /**
17
+ * Create a server-side caller for the tRPC API
18
+ * @example
19
+ * const trpc = createCaller(createContext)
20
+ * const result = await trpc.example.hello({ text: 'world' })
21
+ */
22
+ export const createCaller = createCallerFactory(appRouter)
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod'
2
+ import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc'
3
+ import { posts } from '@/server/db/schema'
4
+ import { eq, desc } from 'drizzle-orm'
5
+
6
+ export const exampleRouter = createTRPCRouter({
7
+ /**
8
+ * Public procedure - accessible without authentication
9
+ */
10
+ hello: publicProcedure
11
+ .input(z.object({ text: z.string().optional() }))
12
+ .query(({ input }) => {
13
+ return {
14
+ greeting: `Hello ${input.text ?? 'World'}!`,
15
+ }
16
+ }),
17
+
18
+ /**
19
+ * Get all posts - public
20
+ */
21
+ getPosts: publicProcedure.query(async ({ ctx }) => {
22
+ const allPosts = await ctx.db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10)
23
+
24
+ return allPosts
25
+ }),
26
+
27
+ /**
28
+ * Get a single post by ID - public
29
+ */
30
+ getPost: publicProcedure.input(z.object({ id: z.number() })).query(async ({ ctx, input }) => {
31
+ const post = await ctx.db.select().from(posts).where(eq(posts.id, input.id)).limit(1)
32
+
33
+ return post[0] ?? null
34
+ }),
35
+
36
+ /**
37
+ * Create a new post - protected (requires authentication)
38
+ */
39
+ createPost: protectedProcedure
40
+ .input(
41
+ z.object({
42
+ title: z.string().min(1).max(256),
43
+ content: z.string().optional(),
44
+ })
45
+ )
46
+ .mutation(async ({ ctx, input }) => {
47
+ const newPost = await ctx.db
48
+ .insert(posts)
49
+ .values({
50
+ title: input.title,
51
+ content: input.content,
52
+ authorId: ctx.session.user.id,
53
+ })
54
+ .returning()
55
+
56
+ return newPost[0]
57
+ }),
58
+
59
+ /**
60
+ * Delete a post - protected
61
+ */
62
+ deletePost: protectedProcedure
63
+ .input(z.object({ id: z.number() }))
64
+ .mutation(async ({ ctx, input }) => {
65
+ await ctx.db.delete(posts).where(eq(posts.id, input.id))
66
+
67
+ return { success: true }
68
+ }),
69
+
70
+ /**
71
+ * Get secret message - protected (requires authentication)
72
+ */
73
+ getSecretMessage: protectedProcedure.query(() => {
74
+ return 'You are authenticated! Here is the secret message.'
75
+ }),
76
+ })
@@ -0,0 +1,67 @@
1
+ import { initTRPC, TRPCError } from '@trpc/server'
2
+ import superjson from 'superjson'
3
+ import { ZodError } from 'zod'
4
+ import { db } from '@/lib/db'
5
+ import { auth } from '@/lib/auth'
6
+
7
+ /**
8
+ * Context creation for tRPC
9
+ * This is called for each incoming request
10
+ */
11
+ export const createTRPCContext = async (opts: { headers: Headers }) => {
12
+ const session = await auth()
13
+
14
+ return {
15
+ db,
16
+ session,
17
+ ...opts,
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Initialize tRPC with context type
23
+ */
24
+ const t = initTRPC.context<typeof createTRPCContext>().create({
25
+ transformer: superjson,
26
+ errorFormatter({ shape, error }) {
27
+ return {
28
+ ...shape,
29
+ data: {
30
+ ...shape.data,
31
+ zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
32
+ },
33
+ }
34
+ },
35
+ })
36
+
37
+ /**
38
+ * Create a server-side caller for tRPC
39
+ */
40
+ export const createCallerFactory = t.createCallerFactory
41
+
42
+ /**
43
+ * Router and procedure helpers
44
+ */
45
+ export const createTRPCRouter = t.router
46
+
47
+ /**
48
+ * Public (unauthenticated) procedure
49
+ */
50
+ export const publicProcedure = t.procedure
51
+
52
+ /**
53
+ * Protected (authenticated) procedure
54
+ * Throws UNAUTHORIZED error if user is not logged in
55
+ */
56
+ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
57
+ if (!ctx.session || !ctx.session.user) {
58
+ throw new TRPCError({ code: 'UNAUTHORIZED' })
59
+ }
60
+
61
+ return next({
62
+ ctx: {
63
+ // Infers the `session` as non-nullable
64
+ session: { ...ctx.session, user: ctx.session.user },
65
+ },
66
+ })
67
+ })