├── .npmrc ├── packages ├── id │ ├── src │ │ ├── index.ts │ │ ├── generate.ts │ │ └── generate.test.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ └── package.json ├── db │ ├── src │ │ ├── consts.ts │ │ ├── index.ts │ │ ├── util │ │ │ └── lifecycle-dates.ts │ │ ├── client.ts │ │ ├── types.ts │ │ └── schema.ts │ ├── tsconfig.json │ ├── drizzle.config.ts │ └── package.json ├── typescript-config │ ├── react-library.json │ ├── package.json │ ├── nextjs.json │ └── base.json └── logs │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── package.json │ └── src │ └── index.ts ├── apps ├── api │ ├── src │ │ ├── pkg │ │ │ ├── errors │ │ │ │ ├── index.ts │ │ │ │ └── error.ts │ │ │ ├── load.ts │ │ │ ├── util │ │ │ │ └── take-unique.ts │ │ │ └── middleware │ │ │ │ ├── jwt-auth.ts │ │ │ │ └── clerk-auth.ts │ │ ├── modules │ │ │ ├── chat │ │ │ │ ├── tools │ │ │ │ │ ├── index.ts │ │ │ │ │ └── weather.tool.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── chat-stream.routes.ts │ │ │ │ └── chats.routes.ts │ │ │ ├── posts │ │ │ │ ├── index.ts │ │ │ │ ├── post.service.ts │ │ │ │ └── post.routes.ts │ │ │ └── webhooks │ │ │ │ └── webhook.routes.ts │ │ └── index.ts │ ├── public │ │ └── favicon.ico │ ├── vercel.json │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── .gitignore │ ├── package.json │ ├── Dockerfile │ └── README.md └── web │ ├── src │ ├── features │ │ └── chat │ │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── use-chat-handlers.ts │ │ │ ├── chat.types.ts │ │ │ ├── index.ts │ │ │ ├── store │ │ │ └── chat-session-store.tsx │ │ │ ├── chat-input │ │ │ ├── button-search.tsx │ │ │ ├── file-list.tsx │ │ │ ├── button-file-upload.tsx │ │ │ └── file-items.tsx │ │ │ ├── kit │ │ │ ├── link-markdown.tsx │ │ │ ├── scroll-button.tsx │ │ │ ├── chat-container.tsx │ │ │ ├── reasoning.tsx │ │ │ ├── code-block.tsx │ │ │ ├── message.tsx │ │ │ ├── prompt-suggestion.tsx │ │ │ ├── markdown.tsx │ │ │ └── sources.tsx │ │ │ ├── utils.ts │ │ │ ├── message.tsx │ │ │ └── conversation.tsx │ ├── app │ │ ├── favicon.ico │ │ ├── posts │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── chat │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ ├── page.tsx │ │ │ │ └── _components │ │ │ │ └── resizable-chat-layout.tsx │ │ ├── (auth) │ │ │ ├── signup │ │ │ │ └── [[...signup]] │ │ │ │ │ └── page.tsx │ │ │ └── signin │ │ │ │ └── [[...signin]] │ │ │ │ └── page.tsx │ │ ├── (marketing) │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── lib │ │ ├── fonts │ │ │ ├── GeistVF.woff │ │ │ └── GeistMonoVF.woff │ │ ├── utils.ts │ │ ├── analytics │ │ │ ├── vercel.tsx │ │ │ └── posthog-client.ts │ │ ├── clerk.ts │ │ ├── fonts.ts │ │ └── fetcher.ts │ ├── components │ │ ├── ui │ │ │ ├── aspect-ratio.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── label.tsx │ │ │ ├── textarea.tsx │ │ │ ├── separator.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── progress.tsx │ │ │ ├── sonner.tsx │ │ │ ├── input.tsx │ │ │ ├── switch.tsx │ │ │ ├── submit-button.tsx │ │ │ ├── avatar.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── card.tsx │ │ │ ├── badge.tsx │ │ │ ├── toggle.tsx │ │ │ ├── popover.tsx │ │ │ ├── alert.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tooltip.tsx │ │ │ ├── slider.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── resizable.tsx │ │ │ ├── accordion.tsx │ │ │ ├── input-otp.tsx │ │ │ ├── table.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── pagination.tsx │ │ │ ├── dialog.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── form.tsx │ │ │ └── sheet.tsx │ │ ├── tailwind-indicator.tsx │ │ ├── hint.tsx │ │ ├── copy-button.tsx │ │ ├── chat │ │ │ └── chat-header.tsx │ │ └── layout │ │ │ └── header.tsx │ ├── providers │ │ ├── theme-provider.tsx │ │ ├── providers.tsx │ │ ├── query-provider.tsx │ │ └── posthog-provider.tsx │ ├── hooks │ │ ├── use-on-mount-unsafe.ts │ │ └── use-mobile.ts │ ├── middleware.ts │ └── api │ │ ├── posts.api.ts │ │ └── client.ts │ ├── postcss.config.mjs │ ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg │ ├── next.config.js │ ├── tsconfig.json │ ├── components.json │ ├── .gitignore │ ├── README.md │ └── package.json ├── pnpm-workspace.yaml ├── .vscode ├── launch.json └── settings.json ├── .gitignore ├── render.yaml ├── package.json ├── turbo.json ├── LICENSE ├── .cursor └── rules │ └── db.mdc ├── biome.jsonc └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/id/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generate"; 2 | -------------------------------------------------------------------------------- /apps/api/src/pkg/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error"; 2 | -------------------------------------------------------------------------------- /packages/db/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const dbPrefix = "db"; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /apps/api/src/modules/chat/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./weather.tool"; 2 | -------------------------------------------------------------------------------- /apps/web/src/features/chat/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-chat-handlers"; 2 | -------------------------------------------------------------------------------- /apps/api/src/modules/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tools"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /apps/api/src/modules/posts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./post.routes"; 2 | export * from "./post.service"; 3 | -------------------------------------------------------------------------------- /apps/api/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sullyo/webapp-starter/HEAD/apps/api/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sullyo/webapp-starter/HEAD/apps/web/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /apps/web/src/lib/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sullyo/webapp-starter/HEAD/apps/web/src/lib/fonts/GeistVF.woff -------------------------------------------------------------------------------- /apps/web/src/app/posts/create/page.tsx: -------------------------------------------------------------------------------- 1 | export default function CreatePostPage() { 2 | return
CreatePostPage
; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/lib/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sullyo/webapp-starter/HEAD/apps/web/src/lib/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/api/src/pkg/load.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import dotenv from "dotenv"; 3 | 4 | const envPath = __dirname + `/../config/.env`; 5 | 6 | if (fs.existsSync(envPath)) { 7 | dotenv.config({ path: envPath }); 8 | } 9 | -------------------------------------------------------------------------------- /packages/db/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "drizzle-orm"; 2 | export { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; 3 | export { db } from "./client"; 4 | export * from "./schema"; 5 | export * from "./types"; 6 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@repo/typescript-config/base.json", 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/id/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@repo/typescript-config/base.json", 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/logs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@repo/typescript-config/base.json", 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "outDir": "dist" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/id/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | reporters: ["default"], 6 | alias: { 7 | "@/": new URL("./src/", import.meta.url).pathname, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/logs/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | reporters: ["default"], 6 | alias: { 7 | "@/": new URL("./src/", import.meta.url).pathname, 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /apps/api/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "api/(.*)", 5 | "destination": "/api" 6 | } 7 | ], 8 | "redirects": [ 9 | { 10 | "source": "/", 11 | "destination": "/api", 12 | "permanent": false 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /apps/api/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | reporters: ["default"], 6 | environment: "node", 7 | alias: { 8 | "@/": new URL("./src/", import.meta.url).pathname, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/db/src/util/lifecycle-dates.ts: -------------------------------------------------------------------------------- 1 | import { timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const lifecycleDates = { 4 | createdAt: timestamp("created_at").defaultNow().notNull(), 5 | updatedAt: timestamp("updated_at") 6 | .defaultNow() 7 | .$onUpdate(() => new Date()), 8 | }; 9 | -------------------------------------------------------------------------------- /apps/web/src/features/chat/chat.types.ts: -------------------------------------------------------------------------------- 1 | import type { UseChatHelpers } from "@ai-sdk/react"; 2 | 3 | import type { ChatUIMessage as InternalMessage } from "../../../../api/src/modules/chat"; 4 | 5 | export type ChatHelpers = UseChatHelpers; 6 | 7 | export type ChatUIMessage = InternalMessage; 8 | -------------------------------------------------------------------------------- /apps/web/src/lib/analytics/vercel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import { SpeedInsights } from "@vercel/speed-insights/next"; 4 | 5 | export function VercelAnalytics() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/analytics/posthog-client.ts: -------------------------------------------------------------------------------- 1 | import { PostHog } from "posthog-node"; 2 | 3 | export function PostHogClient() { 4 | const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 5 | host: process.env.NEXT_PUBLIC_POSTHOG_HOST!, 6 | flushAt: 1, 7 | flushInterval: 0, 8 | }); 9 | return posthogClient; 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; 4 | 5 | function AspectRatio({ ...props }: React.ComponentProps) { 6 | return ; 7 | } 8 | 9 | export { AspectRatio }; 10 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /apps/web/src/lib/clerk.ts: -------------------------------------------------------------------------------- 1 | import { Clerk } from "@clerk/clerk-js"; 2 | 3 | export const clerk = new Clerk(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ?? ""); 4 | 5 | export async function getToken() { 6 | if (typeof document === "undefined") return null; 7 | 8 | await clerk.load(); 9 | const token = await clerk.session?.getToken(); 10 | return token; 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/db/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | config({ path: ".env" }); 5 | 6 | export default defineConfig({ 7 | verbose: true, 8 | schemaFilter: ["public"], 9 | schema: "./src/index.ts", 10 | dialect: "postgresql", 11 | dbCredentials: { 12 | url: process.env.DATABASE_URL!, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import type * as React from "react"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/src/modules/chat/types.ts: -------------------------------------------------------------------------------- 1 | import type { InferUITools, ToolSet, UIDataTypes, UIMessage } from "ai"; 2 | 3 | import { weatherTool } from "@/modules/chat/tools"; 4 | 5 | const customTools: ToolSet = { 6 | weather: weatherTool(), 7 | }; 8 | 9 | export type ChatTools = InferUITools; 10 | 11 | export type ChatUIMessage = UIMessage; 12 | -------------------------------------------------------------------------------- /apps/web/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-on-mount-unsafe.ts: -------------------------------------------------------------------------------- 1 | import type { EffectCallback } from "react"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | export function useOnMountUnsafe(effect: EffectCallback) { 5 | const initialized = useRef(false); 6 | 7 | useEffect(() => { 8 | if (!initialized.current) { 9 | initialized.current = true; 10 | effect(); 11 | } 12 | }, []); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/api/src/modules/posts/post.service.ts: -------------------------------------------------------------------------------- 1 | import { db, type NewPost, posts } from "@repo/db"; 2 | 3 | export const postService = { 4 | async createPost(post: NewPost) { 5 | const newPost = await db.insert(posts).values(post).returning(); 6 | return newPost; 7 | }, 8 | 9 | async getPosts() { 10 | const allPosts = await db.select().from(posts); 11 | return allPosts; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/features/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./chat"; 2 | export * from "./chat-input/button-file-upload"; 3 | export * from "./chat-input/button-search"; 4 | export * from "./chat-input/chat-input"; 5 | export * from "./chat-input/file-items"; 6 | export * from "./chat-input/file-list"; 7 | export * from "./conversation"; 8 | export * from "./hooks"; 9 | export * from "./store/chat-session-store"; 10 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "placehold.co", 8 | }, 9 | { 10 | protocol: "https", 11 | hostname: "images.unsplash.com", 12 | }, 13 | ], 14 | dangerouslyAllowSVG: true, 15 | }, 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [ 6 | { 7 | "name": "next" 8 | } 9 | ], 10 | "module": "ESNext", 11 | "lib": [ 12 | "ES2023" 13 | ], 14 | "moduleResolution": "Bundler", 15 | "allowJs": true, 16 | "jsx": "preserve", 17 | "noEmit": true 18 | } 19 | } -------------------------------------------------------------------------------- /apps/web/src/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Chat } from "@/features/chat"; 4 | 5 | export default function ChatPage() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware } from "@clerk/nextjs/server"; 2 | 3 | export default clerkMiddleware(); 4 | 5 | export const config = { 6 | matcher: [ 7 | // Skip Next.js internals and all static files, unless found in search params 8 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", 9 | // Always run for API routes 10 | "/(api|trpc)(.*)", 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .vercel/ 3 | .yarn/ 4 | !.yarn/releases 5 | .vscode/* 6 | !.vscode/launch.json 7 | !.vscode/*.code-snippets 8 | .idea/workspace.xml 9 | .idea/usage.statistics.xml 10 | .idea/shelf 11 | 12 | # deps 13 | node_modules/ 14 | 15 | # env 16 | .env 17 | .env.production 18 | 19 | # logs 20 | logs/ 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | pnpm-debug.log* 26 | lerna-debug.log* 27 | 28 | # misc 29 | .DS_Store 30 | .vercel 31 | -------------------------------------------------------------------------------- /apps/web/src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Inter as FontSans } from "next/font/google"; 2 | import localFont from "next/font/local"; 3 | 4 | export const fontSans = FontSans({ 5 | subsets: ["latin"], 6 | variable: "--font-sans", 7 | }); 8 | export const geistSans = localFont({ 9 | src: "./fonts/GeistVF.woff", 10 | variable: "--font-geist-sans", 11 | }); 12 | export const geistMono = localFont({ 13 | src: "./fonts/GeistMonoVF.woff", 14 | variable: "--font-geist-mono", 15 | }); 16 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/signup/[[...signup]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignUp } from "@clerk/nextjs"; 4 | import { useTheme } from "next-themes"; 5 | import { dark } from "@clerk/themes"; 6 | 7 | export default function SignUpPage() { 8 | const { theme } = useTheme(); 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/src/app/(auth)/signin/[[...signin]]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SignIn } from "@clerk/nextjs"; 3 | 4 | import { useTheme } from "next-themes"; 5 | import { dark } from "@clerk/themes"; 6 | 7 | export default function SignInPage() { 8 | const { theme } = useTheme(); 9 | 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/layout/header"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface MarketingLayoutProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export default async function MarketingLayout({ children }: MarketingLayoutProps) { 9 | return ( 10 |
11 |
12 |
{children}
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/api/src/pkg/util/take-unique.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from "@/pkg/errors"; 2 | 3 | export function takeUnique(arr: T[]): T { 4 | return arr[0] as T; 5 | } 6 | export function takeUniqueOrNull(arr: T[]): T | null { 7 | return arr[0] ?? null; 8 | } 9 | 10 | export function takeUniqueOrThrow(arr: T[]): T { 11 | if (arr.length === 0) { 12 | throw new InternalServerError("No unique item found in array, this should never happen"); 13 | } 14 | return arr[0] as T; 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "bun", 6 | "internalConsoleOptions": "neverOpen", 7 | "request": "launch", 8 | "name": "Debug API", 9 | "program": "${workspaceFolder}/apps/api/src/index.ts", 10 | "cwd": "${workspaceFolder}/apps/api", 11 | "stopOnEntry": false, 12 | "watchMode": true, 13 | "env": { 14 | "DOTENV_PATH": "${workspaceFolder}/apps/api/.env" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "lib": ["ES2023", "DOM", "DOM.Iterable"], 10 | "paths": { 11 | "@/*": ["./src/*", "../api/src/*"] 12 | } 13 | }, 14 | "include": [ 15 | "**/*.ts", 16 | "**/*.tsx", 17 | "next-env.d.ts", 18 | "next.config.js", 19 | ".next/types/**/*.ts" 20 | ], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/src/api/posts.api.ts: -------------------------------------------------------------------------------- 1 | import { apiRpc, callRpc, getApiClient, type InferRequestType } from "./client"; 2 | 3 | const $createPost = apiRpc.posts.$post; 4 | 5 | export async function getPosts() { 6 | const client = await getApiClient(); 7 | return callRpc(client.posts.$get()); 8 | } 9 | 10 | export type CreatePostParams = InferRequestType; 11 | export async function createPost(params: CreatePostParams) { 12 | const client = await getApiClient(); 13 | return callRpc(client.posts.$post(params)); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/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": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 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 | } 22 | -------------------------------------------------------------------------------- /packages/db/src/client.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/postgres-js"; 2 | import postgres from "postgres"; 3 | import * as schema from "./schema"; 4 | 5 | export function createClient(connectionString: string) { 6 | const client = postgres(connectionString, { prepare: false }); 7 | const drizzleConfig = { 8 | schema, 9 | driver: "pg", 10 | dbCredentials: { 11 | connectionString, 12 | }, 13 | }; 14 | return drizzle(client, drizzleConfig); 15 | } 16 | 17 | export const db = createClient(process.env.DATABASE_URL!); 18 | -------------------------------------------------------------------------------- /apps/web/src/app/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResizableChatLayout } from "./_components/resizable-chat-layout"; 2 | 3 | type Params = Promise<{ id: string }>; 4 | 5 | export default async function ChatPage({ params }: { params: Params }) { 6 | const { id } = await params; 7 | 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/src/lib/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from "@/lib/clerk"; 2 | 3 | export const chatUrl = `${process.env.NEXT_PUBLIC_API_URL}/api/chat`; 4 | 5 | export async function customFetcher(input: RequestInfo | URL, init?: RequestInit) { 6 | const token = await getToken(); 7 | const headers: Record = { 8 | ...(init?.headers as Record), 9 | }; 10 | 11 | if (token) { 12 | headers.authorization = `Bearer ${token}`; 13 | } 14 | 15 | return fetch(input, { 16 | ...init, 17 | headers, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/logs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/logs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "types": "src/index.ts", 7 | "keywords": [], 8 | "scripts": { 9 | "test": "vitest run -c ./vitest.config.ts", 10 | "format": "biome format --write ." 11 | }, 12 | "devDependencies": { 13 | "@repo/typescript-config": "workspace:*", 14 | "@types/node": "^22.15.3", 15 | "typescript": "^5.8.3" 16 | }, 17 | "dependencies": { 18 | "pino": "^9.6.0", 19 | "pino-pretty": "^13.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.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 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /packages/id/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/id", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "types": "src/index.ts", 7 | "keywords": [], 8 | "scripts": { 9 | "test": "vitest run -c ./vitest.config.ts", 10 | "format": "biome format --write ." 11 | }, 12 | "devDependencies": { 13 | "@repo/typescript-config": "workspace:*", 14 | "@types/node": "^22.16.0", 15 | "typescript": "^5.8.3", 16 | "vitest": "^3.2.4", 17 | "ai": "^5.0.4" 18 | }, 19 | "dependencies": { 20 | "base-x": "^5.0.1" 21 | } 22 | } -------------------------------------------------------------------------------- /apps/web/src/providers/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ThemeProviderProps } from "next-themes"; 4 | import { TooltipProvider } from "@/components/ui/tooltip"; 5 | import { QueryProvider } from "@/providers/query-provider"; 6 | import { ThemeProvider } from "@/providers/theme-provider"; 7 | 8 | export function Providers({ children, ...props }: ThemeProviderProps) { 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "module": "NodeNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ES2022" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files (can opt-in for commiting if needed) 29 | .env* 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # clerk configuration (can include secrets) 39 | /.clerk/ 40 | -------------------------------------------------------------------------------- /apps/web/src/providers/query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import * as React from "react"; 5 | 6 | interface QueryProviderProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export function QueryProvider({ children }: QueryProviderProps) { 11 | const [queryClient] = React.useState( 12 | () => 13 | new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | staleTime: 60 * 1000, 17 | }, 18 | }, 19 | }) 20 | ); 21 | 22 | return {children}; 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined); 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 12 | }; 13 | mql.addEventListener("change", onChange); 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 15 | return () => mql.removeEventListener("change", onChange); 16 | }, []); 17 | 18 | return !!isMobile; 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ], 7 | "[javascript]": { 8 | "editor.defaultFormatter": "biomejs.biome" 9 | }, 10 | "[typescript]": { 11 | "editor.defaultFormatter": "biomejs.biome" 12 | }, 13 | "[typescriptreact]": { 14 | "editor.defaultFormatter": "biomejs.biome" 15 | }, 16 | "[json]": { 17 | "editor.defaultFormatter": "vscode.json-language-features" 18 | }, 19 | "[jsonc]": { 20 | "editor.defaultFormatter": "vscode.json-language-features" 21 | }, 22 | "editor.formatOnSave": true, 23 | "editor.codeActionsOnSave": { 24 | "source.fixAll.biome": "explicit" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Label({ className, ...props }: React.ComponentProps) { 9 | return ( 10 | 18 | ); 19 | } 20 | 21 | export { Label }; 22 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | # Render config for the webapp-starter project 2 | services: 3 | - type: web 4 | name: webapp-starter 5 | repo: https://github.com/sullyo/webapp-starter 6 | plan: free 7 | envVars: 8 | - key: BUN_VERSION 9 | value: "1.2.3" 10 | sync: false 11 | - key: CLERK_SIGNING_SECRET 12 | sync: false 13 | - key: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY 14 | sync: false 15 | - key: CLERK_SECRET_KEY 16 | sync: false 17 | - key: DATABASE_URL 18 | sync: false 19 | - key: PORT 20 | value: "3004" 21 | sync: false 22 | region: oregon 23 | buildCommand: pnpm install --frozen-lockfile 24 | startCommand: pnpm start 25 | healthCheckPath: /health 26 | rootDir: apps/api 27 | version: "1" 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp-starter", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "format": "biome format --write .", 9 | "typecheck": "turbo run typecheck", 10 | "db:push": "turbo run db:push", 11 | "db:generate": "turbo run db:generate", 12 | "db:migrate": "turbo run db:migrate", 13 | "db:studio": "turbo run db:studio", 14 | "db:seed": "turbo run db:seed" 15 | }, 16 | "devDependencies": { 17 | "@biomejs/biome": "2.0.6", 18 | "turbo": "^2.5.4", 19 | "typescript": "^5", 20 | "ultracite": "^5.0.32" 21 | }, 22 | "packageManager": "pnpm@9.0.0", 23 | "engines": { 24 | "node": ">=22" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") { 3 | return null; 4 | } 5 | 6 | return ( 7 |
8 |
xs
9 |
sm
10 |
md
11 |
lg
12 |
xl
13 |
2xl
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/features/chat/store/chat-session-store.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import { createContext, useContext, useMemo } from "react"; 5 | 6 | const ChatSessionContext = createContext<{ chatId: string | null | undefined }>({ 7 | chatId: null, 8 | }); 9 | 10 | export const useChatSession = () => useContext(ChatSessionContext); 11 | 12 | export function ChatSessionProvider({ children }: { children: React.ReactNode }) { 13 | const pathname = usePathname(); 14 | const chatId = useMemo(() => { 15 | if (pathname?.startsWith("/chat/")) return pathname.split("/chat/")[1]; 16 | return null; 17 | }, [pathname]); 18 | 19 | return {children}; 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/src/providers/posthog-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import posthog from "posthog-js"; 4 | import { PostHogProvider } from "posthog-js/react"; 5 | import type * as React from "react"; 6 | 7 | if (typeof window !== "undefined") { 8 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 9 | api_host: "/ingest", 10 | person_profiles: "identified_only", 11 | capture_pageview: false, 12 | capture_pageleave: true, 13 | ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com", 14 | loaded: (posthog) => { 15 | if (process.env.NODE_ENV === "development") posthog.debug(); 16 | }, 17 | }); 18 | } 19 | 20 | export function PHProvider({ children }: React.PropsWithChildren) { 21 | return {children}; 22 | } 23 | -------------------------------------------------------------------------------- /packages/id/src/generate.ts: -------------------------------------------------------------------------------- 1 | import baseX from "base-x"; 2 | 3 | const b58 = baseX("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); 4 | 5 | import { createIdGenerator } from "ai"; 6 | 7 | const prefixes = { 8 | post: "post", 9 | file: "file", 10 | chat: "chat", 11 | message: "msg", 12 | } as const; 13 | 14 | export function newId(prefix: TPrefix, size?: number) { 15 | const idGenerator = createIdGenerator({ 16 | prefix: prefixes[prefix], 17 | separator: "-", 18 | size: size ?? 14, 19 | }); 20 | 21 | return idGenerator(); 22 | } 23 | 24 | export function newIdWithoutPrefix(maxLength: number): string { 25 | const buf = crypto.getRandomValues(new Uint8Array(20)); 26 | const encoded = b58.encode(buf); 27 | return encoded.slice(0, maxLength); 28 | } 29 | -------------------------------------------------------------------------------- /packages/logs/src/index.ts: -------------------------------------------------------------------------------- 1 | import pino, { stdTimeFunctions } from "pino"; 2 | 3 | interface LoggerOptions { 4 | level?: string; 5 | prettyPrint?: boolean; 6 | } 7 | 8 | function createLogger({ level = "info", prettyPrint = false }: LoggerOptions = {}) { 9 | const options: pino.LoggerOptions = { 10 | level, 11 | formatters: { 12 | level: (label) => { 13 | return { level: label.toUpperCase() }; 14 | }, 15 | }, 16 | timestamp: stdTimeFunctions.isoTime, 17 | }; 18 | 19 | if (prettyPrint) { 20 | options.transport = { 21 | target: "pino-pretty", 22 | options: { 23 | colorize: true, 24 | }, 25 | }; 26 | } 27 | 28 | return pino(options); 29 | } 30 | 31 | const logger = createLogger({ 32 | prettyPrint: true, 33 | }); 34 | 35 | export { createLogger, logger }; 36 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |