Skip to main content
The Answer Overflow main site is a Next.js 16 application that provides the web interface for browsing Discord Q&A content, managing server dashboards, and AI-powered chat.

Tech Stack

  • Next.js 16 with App Router
  • React 19 with React Compiler enabled
  • TypeScript with strict mode
  • Turbopack for fast dev builds

Project Structure

src/
├── app/
│   └── (main-site)/
│       ├── api/              # API routes (Elysia)
│       │   ├── [[...slugs]]/route.ts   # Unified API handler
│       │   └── handlers/     # Route handlers
│       ├── about/            # About page
│       ├── blog/             # Blog pages
│       ├── browse/           # Browse communities
│       ├── c/                # Channel/server pages
│       │   └── [serverId]/[channelId]/page.tsx
│       ├── chat/             # AI chat interface
│       │   └── [domain]/[repo]/[[...path]]/page.tsx
│       ├── dashboard/        # Server management dashboard
│       │   └── [serverId]/
│       └── page.tsx          # Homepage
├── components/               # Shared React components
├── lib/                      # Utilities and helpers
└── styles/                   # Global styles

Next.js Configuration

File: next.config.ts
const nextConfig: NextConfig = {
  typescript: {
    ignoreBuildErrors: true,  // Using tsgo for faster typechecking
  },
  pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
  transpilePackages: [
    "@packages/ui",
    "@packages/database",
    "@packages/agent"
  ],
  
  // Enable React 19 compiler
  reactCompiler: true,
  
  // Turbopack caching for dev
  experimental: {
    turbopackFileSystemCacheForDev: true,
  },
  
  // Image optimization
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "cdn.discordapp.com" },
      { protocol: "https", hostname: "avatars.githubusercontent.com" },
    ],
  },
};

Key Plugins

  • @vercel/toolbar - Dev toolbar for Vercel features
  • botid - Bot detection for security
  • fumadocs-mdx - MDX support for docs

API Routes

All API routes are unified through Elysia in src/app/(main-site)/api/[[...slugs]]/route.ts:65.

Elysia App Setup

import { openapi } from "@elysiajs/openapi";
import { Elysia } from "elysia";

const app = new Elysia({ prefix: "/api" })
  .use(openapi({
    documentation: {
      info: {
        title: "AnswerOverflow API",
        version: "1.0.0",
      },
      servers: [{ url: "https://www.answeroverflow.com/" }],
      components: {
        securitySchemes: {
          apiKey: {
            type: "apiKey",
            in: "header",
            name: "x-api-key",
          },
        },
      },
    },
  }))
  .post("/v1/webhooks/convex", handleConvexWebhook)
  .post("/v1/webhooks/github", handleGitHubWebhook)
  .all("/auth/*", handleAuth)
  .post("/v1/messages/:id", handleMarkSolution);

Route Handlers

import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";
import { checkBotId } from "botid/server";

const { handler } = convexBetterAuthNextJs({
  convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
  convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});

export async function handleAuth(c: Context) {
  const url = new URL(c.request.url);
  
  // Handle GitHub OAuth setup redirect
  if (url.pathname === "/api/auth/callback/github" &&
      url.searchParams.has("setup_action")) {
    return Response.redirect(new URL("/dashboard/settings", url.origin));
  }
  
  // Block bots from anonymous auth
  const isAnonRoute =
    url.pathname === "/api/auth/anonymous-session" ||
    url.pathname === "/api/auth/sign-in/anonymous";
    
  if (isAnonRoute) {
    const isBot = await checkForBot(c.request);
    if (isBot) {
      return new Response(JSON.stringify({ error: "Access denied" }), {
        status: 403,
      });
    }
  }
  
  // Delegate to Better Auth handler
  if (c.request.method === "GET") return handler.GET(c.request);
  if (c.request.method === "POST") return handler.POST(c.request);
  
  return new Response("Method not allowed", { status: 405 });
}

Authentication

Better Auth Setup

The app uses Better Auth with Convex adapter:
import { convexBetterAuthNextJs } from "@convex-dev/better-auth/nextjs";

const { handler, auth, signIn, signOut } = convexBetterAuthNextJs({
  convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,
  convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
});

Supported Providers

  • GitHub OAuth - Primary authentication
  • Anonymous - Guest sessions for browsing (bot-protected)

Session Management

import { useSession } from "@convex-dev/better-auth/react";

export function UserMenu() {
  const { data: session, status } = useSession();
  
  if (status === "loading") return <Skeleton />;
  if (!session?.user) return <SignInButton />;
  
  return (
    <DropdownMenu>
      <DropdownMenuTrigger>
        <Avatar src={session.user.image} />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem onClick={() => signOut()}>
          Sign Out
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Convex Integration

Client Provider

import { ConvexProvider, ConvexReactClient } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export default function RootLayout({ children }) {
  return (
    <ConvexProvider client={convex}>
      {children}
    </ConvexProvider>
  );
}

Querying Data

import { useQuery } from "convex/react";
import { api } from "@packages/database";

export function ServerDashboard({ serverId }: { serverId: string }) {
  const server = useQuery(api.servers.getByDiscordId, {
    discordId: BigInt(serverId),
  });
  
  const channels = useQuery(api.channels.listByServerId, {
    serverId: server?._id,
  });
  
  if (!server) return <NotFound />;
  
  return (
    <div>
      <h1>{server.name}</h1>
      <ChannelList channels={channels} />
    </div>
  );
}

Mutations

import { useMutation } from "convex/react";
import { api } from "@packages/database";

export function ChannelSettingsForm({ channelId }) {
  const updateChannel = useMutation(api.channels.update);
  
  const handleSubmit = async (data) => {
    await updateChannel({
      channelId,
      markSolutionEnabled: data.markSolutionEnabled,
    });
  };
  
  return <form onSubmit={handleSubmit}>...</form>;
}

Key Pages

Server/Channel Pages

Location: src/app/(main-site)/c/[serverId]/[channelId]/page.tsx Displays Discord messages from a specific channel with SEO optimization.
export async function generateMetadata({ params }) {
  const { serverId, channelId } = params;
  const channel = await convex.query(api.channels.getByDiscordId, {
    discordId: BigInt(channelId),
  });
  
  return {
    title: `${channel.name} - Answer Overflow`,
    description: channel.description,
    openGraph: {
      images: [channel.iconUrl],
    },
  };
}

Dashboard

Location: src/app/(main-site)/dashboard/[serverId]/(dashboard)/ Server management interface for configuring channels, permissions, and settings.
1

Overview

Server stats, activity graphs, top contributors
2

Channels

Enable/disable indexing, mark solution, auto-thread per channel
3

Settings

Configure server-wide preferences like archive on solve, tags, consent

AI Chat

Location: src/app/(main-site)/chat/[domain]/[repo]/[[...path]]/page.tsx AI-powered chat interface for repository documentation.
import { useChat } from "ai/react";

export function ChatInterface({ repo }) {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: "/api/chat",
    body: { repo },
  });
  
  return (
    <div>
      <MessageList messages={messages} />
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
      </form>
    </div>
  );
}

Styling

The app uses Tailwind CSS with custom configuration:
postcss.config.js
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

Theme Configuration

import { ThemeProvider } from "next-themes";

export function Providers({ children }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  );
}

Development

Running Locally

cd apps/main-site
bun run dev          # Start dev server with Turbopack
bun run dev:prod     # Dev server with production env

Environment Variables

# Convex
NEXT_PUBLIC_CONVEX_URL=https://xxx.convex.cloud
NEXT_PUBLIC_CONVEX_SITE_URL=https://xxx.convex.site

# Auth
BETTER_AUTH_SECRET=your_secret
GITHUB_CLIENT_ID=your_github_app_id
GITHUB_CLIENT_SECRET=your_github_secret

# Site
NEXT_PUBLIC_BASE_URL=https://www.answeroverflow.com
NEXT_PUBLIC_DEPLOYMENT_ENV=development

Type Checking

bun run typecheck       # Fast (tsgo)
bun run typecheck:slow  # Thorough (tsc)

Building

bun run build      # Production build
bun run start      # Start production server

Performance Optimizations

  • React Compiler - Automatic memoization
  • Turbopack - Fast dev builds and HMR
  • Component caching - cacheComponents: true
  • Production source maps - For better error tracking

Deployment

Deployed to Vercel with automatic deployments on push to main.
vercel.json
{
  "framework": "nextjs",
  "buildCommand": "bun run build",
  "installCommand": "bun install",
  "env": {
    "NEXT_PUBLIC_CONVEX_URL": "@convex-url",
    "BETTER_AUTH_SECRET": "@auth-secret"
  }
}