diff --git a/.agent/tools/api/better-auth.md b/.agent/tools/api/better-auth.md new file mode 100644 index 00000000..b5651945 --- /dev/null +++ b/.agent/tools/api/better-auth.md @@ -0,0 +1,273 @@ +--- +description: Better Auth - authentication library for Next.js, sessions, OAuth +mode: subagent +tools: + read: true + write: true + edit: true + bash: true + glob: true + grep: true + webfetch: true + task: true + context7_*: true +--- + +# Better Auth - Authentication Library + + + +## Quick Reference + +- **Purpose**: Full-featured authentication for Next.js applications +- **Packages**: `better-auth`, `@better-auth/expo`, `@better-auth/passkey` +- **Docs**: Use Context7 MCP for current documentation + +**Key Features**: +- Email/password, OAuth, magic links, passkeys +- Session management with database storage +- Built-in Drizzle adapter +- React hooks for client-side auth + +**Server Setup**: + +```tsx +// packages/auth/src/server.ts +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "@workspace/db"; + +// Validate required environment variables +const requiredEnvVars = ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', 'BETTER_AUTH_SECRET'] as const; +for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + throw new Error(`Missing required environment variable: ${envVar}`); + } +} + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + }), + emailAndPassword: { + enabled: true, + }, + socialProviders: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }, + }, +}); +``` + +**Client Hooks**: + +```tsx +// packages/auth/src/client/react.ts +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: process.env.NEXT_PUBLIC_APP_URL, +}); + +export const { useSession, signIn, signOut, signUp } = authClient; +``` + +**Usage in Components**: + +```tsx +"use client"; +import { useSession, signIn, signOut } from "@workspace/auth/client/react"; + +function AuthButton() { + const { data: session, isPending } = useSession(); + + if (isPending) return
Loading...
; + + if (session) { + return ( +
+ {session.user.email} + +
+ ); + } + + return ( + // Get from form state + const { email, password } = formData; + + ); +} +``` + +**Protected Routes**: + +```tsx +// middleware.ts +import { auth } from "@workspace/auth/server"; + +export default auth.middleware({ + publicRoutes: ["/", "/login", "/signup"], + redirectTo: "/login", +}); +``` + + + +## Detailed Patterns + +### OAuth Sign In + +```tsx +import { signIn } from "@workspace/auth/client/react"; + +// Google OAuth + + +// GitHub OAuth + +``` + +### Email/Password Sign Up + +```tsx +import { signUp } from "@workspace/auth/client/react"; + +const handleSignUp = async (data: { email: string; password: string; name: string }) => { + const result = await signUp.email({ + email: data.email, + password: data.password, + name: data.name, + }); + + if (result.error) { + console.error(result.error); + return; + } + + // User created and signed in + router.push("/dashboard"); +}; +``` + +### Server-Side Session + +```tsx +// In server component or API route +import { auth } from "@workspace/auth/server"; +import { headers } from "next/headers"; + +export async function getServerSession() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + return session; +} + +// Usage in page +import { redirect } from "next/navigation"; + +export default async function DashboardPage() { + const session = await getServerSession(); + + if (!session) { + redirect("/login"); + } + + return
Welcome, {session.user.name}
; +} +``` + +### Passkey Authentication + +```tsx +// Server config +import { passkey } from "@better-auth/passkey"; + +export const auth = betterAuth({ + plugins: [passkey()], + // ... other config +}); + +// Client usage +import { signIn } from "@workspace/auth/client/react"; + + +``` + +### Custom Session Data + +```tsx +// Extend session with custom fields +export const auth = betterAuth({ + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // Update session every 24 hours + cookieCache: { + enabled: true, + maxAge: 60 * 5, // 5 minutes + }, + }, + user: { + additionalFields: { + role: { + type: "string", + defaultValue: "user", + }, + }, + }, +}); +``` + +### Database Schema Generation + +```bash +# Generate auth schema for Drizzle +pnpm --filter auth db:generate + +# This creates/updates packages/db/src/schema/auth.ts +``` + +### API Route Handler + +```tsx +// app/api/auth/[...all]/route.ts +import { auth } from "@workspace/auth/server"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth); +``` + +## Common Mistakes + +1. **Missing environment variables** + - `BETTER_AUTH_SECRET` required + - OAuth credentials for each provider + +2. **Not generating schema** + - Run `db:generate` after auth config changes + - Auth tables must exist in database + +3. **Forgetting to await headers()** + - `headers()` is async in Next.js 15+ + - Always `await headers()` before passing to auth + +4. **Client/server import confusion** + - Use `@workspace/auth/server` on server + - Use `@workspace/auth/client/react` on client + +## Related + +- `tools/api/drizzle.md` - Database adapter +- `tools/ui/nextjs-layouts.md` - Protected layouts +- Context7 MCP for Better Auth documentation diff --git a/.agent/tools/api/drizzle.md b/.agent/tools/api/drizzle.md new file mode 100644 index 00000000..11250c50 --- /dev/null +++ b/.agent/tools/api/drizzle.md @@ -0,0 +1,284 @@ +--- +description: Drizzle ORM - type-safe database queries, migrations, schema +mode: subagent +tools: + read: true + write: true + edit: true + bash: true + glob: true + grep: true + webfetch: true + task: true + context7_*: true +--- + +# Drizzle ORM - Type-Safe Database + + + +## Quick Reference + +- **Purpose**: Type-safe ORM for SQL databases +- **Packages**: `drizzle-orm`, `drizzle-kit`, `drizzle-zod` +- **Docs**: Use Context7 MCP for current documentation + +**Key Features**: +- Full TypeScript inference from schema +- SQL-like query builder +- Zero dependencies at runtime +- Automatic migrations + +**Schema Definition**: + +```tsx +// packages/db/src/schema/users.ts +import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; + +export const users = pgTable("users", { + id: uuid("id").primaryKey().defaultRandom(), + email: text("email").notNull().unique(), + name: text("name"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), // Note: only sets on INSERT; use .$onUpdate() or DB trigger for UPDATE +}); +``` + +**Basic Queries**: + +```tsx +import { db } from "@workspace/db"; +import { users } from "@workspace/db/schema"; +import { eq } from "drizzle-orm"; + +// Select all +const allUsers = await db.select().from(users); + +// Select with filter +const user = await db + .select() + .from(users) + .where(eq(users.email, "test@example.com")) + .limit(1); + +// Insert +const newUser = await db + .insert(users) + .values({ email: "new@example.com", name: "New User" }) + .returning(); + +const userId = "user-id-to-update"; // From request params, auth, etc. + +// Update +await db + .update(users) + .set({ name: "Updated Name" }) + .where(eq(users.id, userId)); + +// Delete +await db.delete(users).where(eq(users.id, userId)); +``` + +**Migration Commands**: + +```bash +# Generate migration from schema changes +pnpm db:generate + +# Apply migrations +pnpm db:migrate + +# Push schema directly (dev only) +pnpm db:push + +# Open Drizzle Studio +pnpm db:studio +``` + +**Zod Integration**: + +```tsx +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { users } from "./schema"; + +export const insertUserSchema = createInsertSchema(users); +export const selectUserSchema = createSelectSchema(users); + +// Use in API validation +app.post("/users", zValidator("json", insertUserSchema), async (c) => { + const data = c.req.valid("json"); + const user = await db.insert(users).values(data).returning(); + return c.json(user[0]); +}); +``` + + + +## Detailed Patterns + +### Relations + +```tsx +import { relations } from "drizzle-orm"; +import { pgTable, text, uuid } from "drizzle-orm/pg-core"; + +// Simplified schema for relations example (see full schema above) +export const users = pgTable("users", { + id: uuid("id").primaryKey().defaultRandom(), + name: text("name"), +}); + +export const posts = pgTable("posts", { + id: uuid("id").primaryKey().defaultRandom(), + title: text("title").notNull(), + authorId: uuid("author_id").references(() => users.id), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + posts: many(posts), +})); + +export const postsRelations = relations(posts, ({ one }) => ({ + author: one(users, { + fields: [posts.authorId], + references: [users.id], + }), +})); +``` + +### Query with Relations + +```tsx +// Using query API (recommended for relations) +const usersWithPosts = await db.query.users.findMany({ + with: { + posts: true, + }, +}); + +// Nested relations +const usersWithPostsAndComments = await db.query.users.findMany({ + with: { + posts: { + with: { + comments: true, + }, + }, + }, +}); +``` + +### Complex Queries + +```tsx +import { and, or, like, gt, desc, sql } from "drizzle-orm"; + +// Multiple conditions +const results = await db + .select() + .from(users) + .where( + and( + like(users.email, "%@example.com"), + gt(users.createdAt, new Date("2024-01-01")) + ) + ) + .orderBy(desc(users.createdAt)) + .limit(10); + +// Raw SQL when needed +const count = await db + .select({ count: sql`count(*)` }) + .from(users); +``` + +### Transactions + +```tsx +await db.transaction(async (tx) => { + const user = await tx + .insert(users) + .values({ email: "test@example.com" }) + .returning(); + + await tx.insert(posts).values({ + title: "First Post", + authorId: user[0].id, + }); +}); +``` + +### Database Connection + +```tsx +// packages/db/src/server.ts +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "./schema"; + +const client = postgres(process.env.DATABASE_URL!); +export const db = drizzle(client, { schema }); +``` + +### Seeding + +```tsx +// packages/db/src/scripts/seed.ts +import { db } from "../server"; +import { users, posts } from "../schema"; + +async function seed() { + // Safety: prevent accidental production seeding + if (process.env.NODE_ENV === "production") { + throw new Error("Seeding is disabled in production. Set NODE_ENV=development."); + } + + console.log("Seeding database..."); + + // Clear existing data + await db.delete(posts); + await db.delete(users); + + // Insert seed data + const [user] = await db + .insert(users) + .values({ email: "admin@example.com", name: "Admin" }) + .returning(); + + await db.insert(posts).values([ + { title: "First Post", authorId: user.id }, + { title: "Second Post", authorId: user.id }, + ]); + + console.log("Seeding complete!"); +} + +seed().catch((err) => { + console.error(err); + process.exit(1); +}); +``` + +## Common Mistakes + +1. **Forgetting `.returning()`** + - Insert/update don't return data by default + - Add `.returning()` to get inserted/updated rows + +2. **Not using transactions** + - Related inserts should be in transaction + - Prevents partial data on failure + +3. **Schema drift** + - Always run `db:generate` after schema changes + - Review generated SQL before applying + +4. **Missing indexes** + - Add indexes for frequently queried columns + - Use `.index()` in schema definition + +## Related + +- `tools/api/hono.md` - API routes using Drizzle +- `workflows/sql-migrations.md` - Migration best practices +- Context7 MCP for Drizzle documentation diff --git a/.agent/tools/api/hono.md b/.agent/tools/api/hono.md new file mode 100644 index 00000000..767f3a1a --- /dev/null +++ b/.agent/tools/api/hono.md @@ -0,0 +1,262 @@ +--- +description: Hono web framework - API routes, middleware, validation +mode: subagent +tools: + read: true + write: true + edit: true + bash: true + glob: true + grep: true + webfetch: true + task: true + context7_*: true +--- + +# Hono - Lightweight Web Framework + + + +## Quick Reference + +- **Purpose**: Fast, lightweight web framework for building APIs +- **Use Case**: API routes in Next.js, edge functions, serverless +- **Docs**: Use Context7 MCP for current documentation + +**Key Features**: +- TypeScript-first with full type inference +- Works on Edge, Node.js, Bun, Deno, Cloudflare Workers +- Built-in middleware (CORS, auth, validation) +- RPC-style client with type safety + +**Common Patterns**: + +```tsx +// Basic route +import { Hono } from "hono"; + +const app = new Hono(); + +app.get("/api/users", (c) => { + return c.json({ users: [] }); +}); + +app.post("/api/users", async (c) => { + const body = await c.req.json(); + return c.json({ created: body }, 201); +}); +``` + +**Zod Validation**: + +```tsx +import { zValidator } from "@hono/zod-validator"; +import { z } from "zod"; + +const createUserSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), +}); + +app.post( + "/api/users", + zValidator("json", createUserSchema), + async (c) => { + const data = c.req.valid("json"); // Typed! + return c.json({ user: data }); + } +); +``` + +**RPC Client** (type-safe API calls): + +```tsx +// Server: Define routes with types +const routes = app + .get("/api/users", (c) => c.json({ users: [] })) + .post("/api/users", zValidator("json", createUserSchema), (c) => { + return c.json({ user: c.req.valid("json") }); + }); + +export type AppType = typeof routes; + +// Client: Full type inference +import { hc } from "hono/client"; +import type { AppType } from "./server"; + +const client = hc("/"); +const res = await client.api.users.$get(); +const data = await res.json(); // Typed! +``` + +**Next.js Integration**: + +```tsx +// app/api/[[...route]]/route.ts +import { Hono } from "hono"; +import { handle } from "hono/vercel"; + +const app = new Hono().basePath("/api"); + +app.get("/hello", (c) => c.json({ message: "Hello!" })); + +export const GET = handle(app); +export const POST = handle(app); +``` + + + +## Detailed Patterns + +### Middleware + +```tsx +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; +import { timing } from "hono/timing"; + +const app = new Hono(); + +// Global middleware +app.use("*", logger()); +app.use("*", timing()); +app.use("/api/*", cors()); + +// Route-specific middleware +app.use("/api/admin/*", async (c, next) => { + const token = c.req.header("Authorization"); + if (!token) { + return c.json({ error: "Unauthorized" }, 401); + } + // TODO: Validate token (JWT verification, session lookup, etc.) + // const user = await verifyToken(token.replace("Bearer ", "")); + // if (!user) return c.json({ error: "Invalid token" }, 401); + await next(); +}); +``` + +### Error Handling + +```tsx +import { HTTPException } from "hono/http-exception"; + +app.onError((err, c) => { + if (err instanceof HTTPException) { + return err.getResponse(); + } + console.error(err); + return c.json({ error: "Internal Server Error" }, 500); +}); + +// Throwing errors +app.get("/api/users/:id", async (c) => { + const user = await getUser(c.req.param("id")); // Application-specific user lookup + if (!user) { + throw new HTTPException(404, { message: "User not found" }); + } + return c.json(user); +}); +``` + +### Grouped Routes + +```tsx +const app = new Hono(); + +// User routes +const users = new Hono() + .get("/", (c) => c.json({ users: [] })) + .get("/:id", (c) => c.json({ id: c.req.param("id") })) + .post("/", (c) => c.json({ created: true })); + +// Mount under /api/users +app.route("/api/users", users); +``` + +### Context Variables + +```tsx +// Set variables in middleware +app.use("*", async (c, next) => { + const user = await getAuthUser(c.req.header("Authorization")); // Verify token and return user + c.set("user", user); + await next(); +}); + +// Access in routes +app.get("/api/profile", (c) => { + const user = c.get("user"); + return c.json(user); +}); +``` + +### Streaming Responses + +```tsx +import { streamText } from "hono/streaming"; + +app.get("/api/stream", (c) => { + return streamText(c, async (stream) => { + for (let i = 0; i < 10; i++) { + await stream.write(`data: ${i}\n\n`); + await stream.sleep(100); + } + }); +}); +``` + +### File Uploads + +```tsx +app.post("/api/upload", async (c) => { + const body = await c.req.parseBody(); + const file = body["file"] as File; + + if (!file) { + return c.json({ error: "No file" }, 400); + } + + // Validate file size (10MB limit) + const MAX_SIZE = 10 * 1024 * 1024; + if (file.size > MAX_SIZE) { + return c.json({ error: "File too large" }, 413); + } + + // Validate MIME type + const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; + if (!ALLOWED_TYPES.includes(file.type)) { + return c.json({ error: "Invalid file type" }, 400); + } + + const buffer = await file.arrayBuffer(); + // Process file... + + return c.json({ filename: file.name, size: file.size }); +}); +``` + +## Common Mistakes + +1. **Forgetting to export route types** + - RPC client needs `AppType` export + - Export at end of route file + +2. **Not awaiting `next()`** + - Middleware must `await next()` + - Otherwise response may be sent early + +3. **Wrong validator target** + - `zValidator("json", schema)` for body + - `zValidator("query", schema)` for query params + - `zValidator("param", schema)` for URL params + +4. **Missing basePath in Next.js** + - Use `.basePath("/api")` when mounting at `/api` + - Routes are relative to basePath + +## Related + +- `tools/api/vercel-ai-sdk.md` - AI streaming with Hono +- `tools/api/drizzle.md` - Database queries in routes +- Context7 MCP for Hono documentation diff --git a/.agent/tools/api/vercel-ai-sdk.md b/.agent/tools/api/vercel-ai-sdk.md new file mode 100644 index 00000000..14d913e7 --- /dev/null +++ b/.agent/tools/api/vercel-ai-sdk.md @@ -0,0 +1,340 @@ +--- +description: Vercel AI SDK - streaming chat, useChat hook, AI providers +mode: subagent +tools: + read: true + write: true + edit: true + bash: true + glob: true + grep: true + webfetch: true + task: true + context7_*: true +--- + +# Vercel AI SDK - AI Integration + + + +## Quick Reference + +- **Purpose**: Build AI-powered applications with streaming support +- **Packages**: `ai`, `@ai-sdk/react`, `@ai-sdk/openai` +- **Docs**: Use Context7 MCP for current documentation + +**Key Components**: +- `useChat` - React hook for chat interfaces +- `streamText` - Server-side streaming +- Provider adapters (OpenAI, Anthropic, etc.) + +**Basic Chat Implementation**: + +```tsx +// Client: useChat hook +"use client"; +import { useChat } from "@ai-sdk/react"; + +function Chat() { + const { messages, input, handleInputChange, handleSubmit, status } = useChat({ + api: "/api/chat", + }); + + return ( +
+ {messages.map((m) => ( +
+ {m.role}: {m.parts.map(p => p.type === "text" ? p.text : null)} +
+ ))} +
+ + +
+
+ ); +} +``` + +```tsx +// Server: API route +import { openai } from "@ai-sdk/openai"; +import { streamText } from "ai"; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const result = streamText({ + model: openai("gpt-4o"), + messages, + }); + + return result.toDataStreamResponse(); +} +``` + +**Custom Transport** (for Hono/custom APIs): + +```tsx +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; + +const { messages, sendMessage, status } = useChat({ + transport: new DefaultChatTransport({ + api: "/api/ai/chat", + }), +}); +``` + +**Message Parts** (for rich content): + +```tsx +messages.map((message) => ( +
+ {message.parts.map((part, i) => { + if (part.type === "text") { + return

{part.text}

; + } + if (part.type === "tool-call") { + return ; {/* Your custom component to render tool output */} + } + return null; + })} +
+)); +``` + +**Status Values**: + +| Status | Meaning | +|--------|---------| +| `idle` | No request in progress | +| `submitted` | Request sent, waiting for response | +| `streaming` | Receiving streamed response | +| `error` | Request failed | + + + +## Detailed Patterns + +### Full Chat Component + +```tsx +"use client"; + +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; +import { marked } from "marked"; +import DOMPurify from "dompurify"; +import { useState, useRef, useEffect } from "react"; + +const sanitizeMarkdown = (text: string) => { + const html = marked.parse(text) as string; + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'a', 'h1', 'h2', 'h3', 'blockquote'], + ALLOWED_ATTR: ['href', 'class'] + }); +}; + +export function AIChatSidebar() { + const [input, setInput] = useState(""); + const scrollRef = useRef(null); + + const { messages, error, sendMessage, status } = useChat({ + transport: new DefaultChatTransport({ + api: "/api/ai/chat", + }), + onError: (err) => console.error("Chat Error:", err), + }); + + // Filter to user/assistant messages only + const displayMessages = messages.filter((m) => + ["assistant", "user"].includes(m.role) + ); + + const isLoading = ["submitted", "streaming"].includes(status); + + // Auto-scroll on new messages + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + const handleSubmit = () => { + if (input.trim()) { + sendMessage({ text: input }); + setInput(""); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + if (error) { + return
Error: {error.message}
; + } + + return ( +
+
+ {displayMessages.map((message) => ( +
+ {message.parts.map((part, i) => { + if (part.type === "text") { + return message.role === "assistant" ? ( +
+ ) : ( +

{part.text}

+ ); + } + return null; + })} +
+ ))} + {isLoading &&
Thinking...
} +
+
+