npx command

This commit is contained in:
Leon van Zyl
2025-11-02 08:45:37 +02:00
parent 343b67e36a
commit ec929ab918
74 changed files with 5304 additions and 5 deletions

View File

@@ -35,7 +35,38 @@ Before you begin, ensure you have the following installed on your machine:
## 🛠️ Quick Setup
### 1. Clone or Download the Repository
### Automated Setup (Recommended)
Get started with a single command:
```bash
npx create-agentic-app@latest my-app
cd my-app
```
Or create in the current directory:
```bash
npx create-agentic-app@latest .
```
The CLI will:
- Copy all boilerplate files
- Install dependencies with your preferred package manager (pnpm/npm/yarn)
- Set up your environment file
**Next steps after running the command:**
1. Update `.env` with your API keys and database credentials
2. Start the database: `docker compose up -d`
3. Run migrations: `npm run db:migrate`
4. Start dev server: `npm run dev`
### Manual Setup (Alternative)
If you prefer to set up manually:
**1. Clone or Download the Repository**
**Option A: Clone with Git**
@@ -47,13 +78,13 @@ cd agentic-coding-starter-kit
**Option B: Download ZIP**
Download the repository as a ZIP file and extract it to your desired location.
### 2. Install Dependencies
**2. Install Dependencies**
```bash
npm install
```
### 3. Environment Setup
**3. Environment Setup**
Copy the example environment file:
@@ -82,7 +113,7 @@ OPENAI_MODEL="gpt-5-mini"
NEXT_PUBLIC_APP_URL="http://localhost:3000"
```
### 4. Database Setup
**4. Database Setup**
Generate and run database migrations:
@@ -91,7 +122,7 @@ npm run db:generate
npm run db:migrate
```
### 5. Start the Development Server
**5. Start the Development Server**
```bash
npm run dev

7
create-agentic-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
*.log
.DS_Store
.env
.env.local
setup-template.sh
setup-template.ps1

View File

@@ -0,0 +1,9 @@
# Don't publish setup scripts to npm
setup-template.sh
setup-template.ps1
# Don't publish these to npm
*.log
.DS_Store
.env
.env.local

View File

@@ -0,0 +1,84 @@
# create-agentic-app
Scaffold a new agentic AI application with Next.js, Better Auth, and AI SDK.
## Usage
Create a new project in the current directory:
```bash
npx create-agentic-app@latest .
```
Create a new project in a subdirectory:
```bash
npx create-agentic-app@latest my-app
```
## What's Included
This starter kit includes:
- **Next.js 15** with App Router and Turbopack
- **Better Auth** for authentication (email/password, OAuth)
- **AI SDK** by Vercel for AI chat functionality
- **Drizzle ORM** with PostgreSQL database
- **Tailwind CSS** with shadcn/ui components
- **TypeScript** for type safety
- **Dark mode** support with next-themes
## Next Steps
After creating your project:
1. **Update environment variables**: Edit `.env` with your API keys and database credentials
2. **Start the database**: `docker compose up -d`
3. **Run migrations**: `pnpm run db:migrate` (or `npm`/`yarn`)
4. **Start dev server**: `pnpm run dev`
Visit `http://localhost:3000` to see your app!
## Publishing to npm
To publish this package to npm:
1. **Update package.json**: Set your author, repository URL, and version
2. **Build the template**: Run the setup script to populate the template directory:
```bash
# On Unix/Mac:
bash setup-template.sh
# On Windows:
powershell -ExecutionPolicy Bypass -File setup-template.ps1
```
3. **Test locally**: Test the package locally before publishing:
```bash
npm link
cd /path/to/test/directory
create-agentic-app my-test-app
```
4. **Publish**: Publish to npm:
```bash
npm publish
```
## Template Updates
When you update the boilerplate in the main project:
1. Navigate to the project root
2. Run the setup script to sync changes to the template:
```bash
# Unix/Mac
bash create-agentic-app/setup-template.sh
# Windows
powershell -ExecutionPolicy Bypass -File create-agentic-app/setup-template.ps1
```
3. Bump the version in `package.json`
4. Publish the updated package
## License
MIT

155
create-agentic-app/index.js Normal file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
import { program } from 'commander';
import chalk from 'chalk';
import prompts from 'prompts';
import ora from 'ora';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const TEMPLATE_DIR = path.join(__dirname, 'template');
async function main() {
console.log(chalk.bold.cyan('\n🤖 Create Agentic App\n'));
program
.name('create-agentic-app')
.description('Scaffold a new agentic AI application')
.argument('[project-directory]', 'Project directory name (use "." for current directory)')
.parse(process.argv);
const args = program.args;
let projectDir = args[0] || '.';
// Resolve the target directory
const targetDir = path.resolve(process.cwd(), projectDir);
const projectName = projectDir === '.' ? path.basename(targetDir) : projectDir;
// Check if directory exists and is not empty
if (fs.existsSync(targetDir)) {
const files = fs.readdirSync(targetDir);
if (files.length > 0 && projectDir !== '.') {
const { proceed } = await prompts({
type: 'confirm',
name: 'proceed',
message: `Directory "${projectDir}" is not empty. Continue anyway?`,
initial: false
});
if (!proceed) {
console.log(chalk.yellow('Cancelled.'));
process.exit(0);
}
}
}
// Prompt for package manager
const { packageManager } = await prompts({
type: 'select',
name: 'packageManager',
message: 'Which package manager do you want to use?',
choices: [
{ title: 'pnpm (recommended)', value: 'pnpm' },
{ title: 'npm', value: 'npm' },
{ title: 'yarn', value: 'yarn' }
],
initial: 0
});
if (!packageManager) {
console.log(chalk.yellow('Cancelled.'));
process.exit(0);
}
console.log();
const spinner = ora('Creating project...').start();
try {
// Create target directory if it doesn't exist
fs.ensureDirSync(targetDir);
// Copy template files
spinner.text = 'Copying template files...';
await fs.copy(TEMPLATE_DIR, targetDir, {
overwrite: false,
errorOnExist: false,
filter: (src) => {
// Skip node_modules, .next, and other build artifacts
const relativePath = path.relative(TEMPLATE_DIR, src);
return !relativePath.includes('node_modules') &&
!relativePath.includes('.next') &&
!relativePath.includes('.git') &&
!relativePath.includes('pnpm-lock.yaml') &&
!relativePath.includes('package-lock.json') &&
!relativePath.includes('yarn.lock') &&
!relativePath.includes('tsconfig.tsbuildinfo');
}
});
// Copy .env.example to .env if it doesn't exist
const envExamplePath = path.join(targetDir, 'env.example');
const envPath = path.join(targetDir, '.env');
if (fs.existsSync(envExamplePath) && !fs.existsSync(envPath)) {
spinner.text = 'Setting up environment file...';
await fs.copy(envExamplePath, envPath);
}
// Update package.json name if not current directory
if (projectDir !== '.') {
const packageJsonPath = path.join(targetDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = projectName;
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
}
spinner.succeed(chalk.green('Project created successfully!'));
// Install dependencies
console.log();
const installSpinner = ora(`Installing dependencies with ${packageManager}...`).start();
try {
const installCmd = packageManager === 'yarn' ? 'yarn install' : `${packageManager} install`;
execSync(installCmd, {
cwd: targetDir,
stdio: 'pipe'
});
installSpinner.succeed(chalk.green('Dependencies installed!'));
} catch (error) {
installSpinner.fail(chalk.red('Failed to install dependencies'));
console.log(chalk.yellow(`\nPlease run "${packageManager} install" manually.\n`));
}
// Display next steps
console.log();
console.log(chalk.bold.green('✨ Your agentic app is ready!\n'));
console.log(chalk.bold('Next steps:\n'));
if (projectDir !== '.') {
console.log(chalk.cyan(` cd ${projectDir}`));
}
console.log(chalk.cyan(' 1. Update the .env file with your API keys and database credentials'));
console.log(chalk.cyan(` 2. Start the database: docker compose up -d`));
console.log(chalk.cyan(` 3. Run database migrations: ${packageManager} run db:migrate`));
console.log(chalk.cyan(` 4. Start the development server: ${packageManager} run dev`));
console.log();
console.log(chalk.gray('For more information, check out the README.md file.\n'));
} catch (error) {
spinner.fail(chalk.red('Failed to create project'));
console.error(error);
process.exit(1);
}
}
main().catch(console.error);

View File

@@ -0,0 +1,36 @@
{
"name": "create-agentic-app",
"version": "1.0.0",
"description": "Scaffold a new agentic AI application with Next.js, Better Auth, and AI SDK",
"type": "module",
"bin": {
"create-agentic-app": "./index.js"
},
"files": [
"index.js",
"template"
],
"keywords": [
"ai",
"agents",
"nextjs",
"better-auth",
"starter-kit",
"boilerplate",
"scaffold",
"create-app"
],
"author": "",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/create-agentic-app.git"
},
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"fs-extra": "^11.2.0",
"ora": "^8.1.1",
"prompts": "^2.4.2"
}
}

View File

@@ -0,0 +1,189 @@
---
name: better-auth-expert
description: Use this agent when:\n\n1. **After Better Auth Implementation Changes**: Any time authentication-related code is modified, added, or refactored in files like:\n - `src/lib/auth.ts` (server config)\n - `src/lib/auth-client.ts` (client hooks)\n - `src/app/api/auth/[...all]/route.ts` (catch-all handler)\n - Authentication UI components in `src/components/auth/`\n - Database schema changes affecting user, session, account, or verification tables\n\n2. **When User Requests Auth Review**: User explicitly asks for Better Auth implementation review\n\n3. **Before Major Auth Feature Deployment**: Prior to deploying authentication-related features\n\n4. **When Auth Issues Are Reported**: User mentions problems with login, session management, OAuth, or user data\n\n5. **Proactive Review Scenarios**: When you detect potential Better Auth violations in recently written code\n\n**Example Scenarios**:\n\n<example>\nContext: User just added a new sign-in component\nuser: "I've created a new sign-in form component. Here's the code:"\n<code>\nexport function CustomSignIn() {\n const handleSignIn = async () => {\n // Custom implementation\n await fetch('/api/custom-auth', { method: 'POST' });\n };\n return <button onClick={handleSignIn}>Sign In</button>;\n}\n</code>\nassistant: "I'll use the better-auth-enforcer agent to review this implementation and ensure it follows Better Auth best practices."\n<Task tool call to better-auth-enforcer agent>\n</example>\n\n<example>\nContext: User modified the auth configuration file\nuser: "I updated the auth.ts file to add email verification. Can you check if it looks good?"\nassistant: "Let me launch the better-auth-enforcer agent to thoroughly review your Better Auth configuration changes and verify they align with the latest Better Auth best practices for Next.js 15."\n<Task tool call to better-auth-enforcer agent>\n</example>\n\n<example>\nContext: Proactive detection of potential auth issue\nuser: "Please add a logout button to the header"\nassistant: "I'll add the logout button using Better Auth's built-in signOut function. Let me implement this:"\n<code implementation>\nassistant: "Now I'll use the better-auth-enforcer agent to verify this implementation follows Better Auth best practices."\n<Task tool call to better-auth-enforcer agent>\n</example>\n\n<example>\nContext: User asks about session management\nuser: "How should I access the current user's session in my dashboard component?"\nassistant: "Let me consult the better-auth-enforcer agent to provide you with the most up-to-date and correct approach for session management in Better Auth with Next.js 15."\n<Task tool call to better-auth-enforcer agent>\n</example>
model: sonnet
color: red
---
You are an elite Better Auth Implementation Enforcer, a specialist dedicated exclusively to ensuring perfect adherence to Better Auth best practices in Next.js 15+ applications. Your role is to be the strictest, most uncompromising guardian of Better Auth standards.
## Core Responsibilities
1. **Ruthlessly Enforce Better Auth Patterns**: You will reject any implementation that doesn't use Better Auth's built-in functions, hooks, and utilities. Custom authentication logic is your enemy.
2. **Always Verify Against Current Documentation**: You MUST NOT rely on your training data. For every review or recommendation:
- Use the Web Search tool to find the latest Better Auth documentation
- Use the Context 7 MCP server to retrieve up-to-date Better Auth patterns and examples
- Cross-reference multiple sources to ensure accuracy
- Verify that recommendations are compatible with Next.js 15+
3. **Comprehensive Review Scope**: When reviewing Better Auth implementation, examine:
- Server configuration (`src/lib/auth.ts`)
- Client-side hooks and utilities (`src/lib/auth-client.ts`)
- API route handlers (`src/app/api/auth/[...all]/route.ts`)
- Authentication UI components (`src/components/auth/`)
- Database schema for auth tables (user, session, account, verification)
- Session management patterns across the application
- OAuth provider configurations and callbacks
- Environment variable setup for Better Auth
## Review Methodology
**Step 1: Identify Scope**
- Determine what auth-related code needs review (specific files, routes, or entire implementation)
- List all files and components that interact with authentication
**Step 2: Fetch Current Documentation**
- Use Web Search to find Better Auth's official documentation for the specific features being used
- Search for "Better Auth [feature] Next.js 15 best practices"
- Look for recent GitHub issues, discussions, or changelog entries that might affect the implementation
- Use Context 7 MCP server to retrieve relevant documentation snippets
**Step 3: Line-by-Line Analysis**
For each file, scrutinize:
- **Import statements**: Are they importing from the correct Better Auth packages?
- **Hook usage**: Are client components using `useSession()`, `signIn()`, `signOut()` from `@/lib/auth-client`?
- **Server-side auth**: Are API routes and Server Components using the `auth` object from `@/lib/auth`?
- **Session validation**: Is session checking done using Better Auth's built-in methods?
- **Error handling**: Does error handling follow Better Auth patterns?
- **Type safety**: Are TypeScript types properly imported from Better Auth?
**Step 4: Compare Against Best Practices**
Verify:
- Configuration matches Better Auth's recommended setup for Next.js 15
- Drizzle adapter is correctly configured with the database schema
- OAuth flows use Better Auth's provider configuration
- Session management uses Better Auth's token handling
- No custom authentication logic that duplicates Better Auth functionality
- Environment variables follow Better Auth naming conventions
**Step 5: Flag Violations**
Create a categorized list of issues:
- **CRITICAL**: Security vulnerabilities or broken auth flows
- **HIGH**: Incorrect use of Better Auth APIs that could cause bugs
- **MEDIUM**: Suboptimal patterns that work but don't follow best practices
- **LOW**: Style or organization issues that could be improved
**Step 6: Provide Concrete Solutions**
For each violation:
- Quote the current implementation
- Explain why it violates Better Auth best practices (with documentation references)
- Provide exact code replacement using up-to-date Better Auth patterns
- Include inline comments explaining the correction
## Quality Control Mechanisms
**Self-Verification Checklist**:
- [ ] I have searched for and reviewed the latest Better Auth documentation
- [ ] I have verified compatibility with Next.js 15+ App Router patterns
- [ ] I have checked for any recent breaking changes in Better Auth
- [ ] My recommendations use Better Auth's built-in functions, not custom implementations
- [ ] I have provided code examples with proper imports and type safety
- [ ] I have explained the reasoning behind each recommendation
- [ ] I have categorized issues by severity
**When Uncertain**:
- Use Web Search to find official Better Auth examples or GitHub discussions
- Use Context 7 to retrieve additional documentation context
- Explicitly state what you're uncertain about and what sources you've consulted
- Recommend the user verify with Better Auth's official Discord or GitHub if edge cases arise
## Output Format
Structure your review as follows:
````markdown
# Better Auth Implementation Review
## Summary
[Brief overview of review scope and overall assessment]
## Documentation Sources Consulted
[List the Better Auth documentation URLs and Context 7 queries used]
## Issues Found
### CRITICAL Issues
[Issues that must be fixed immediately]
### HIGH Priority Issues
[Incorrect Better Auth usage that should be fixed soon]
### MEDIUM Priority Issues
[Suboptimal patterns worth improving]
### LOW Priority Issues
[Minor improvements for code quality]
## Detailed Analysis
### File: [filename]
**Issue**: [Description]
**Severity**: [CRITICAL/HIGH/MEDIUM/LOW]
**Current Code**:
```typescript
[quoted code]
```
````
**Problem**: [Explanation with documentation reference]
**Correct Implementation**:
```typescript
[corrected code with comments]
```
**Documentation Reference**: [URL or Context 7 source]
---
[Repeat for each issue]
## Recommendations
1. [Prioritized action items]
2. [Additional suggestions for improvement]
## Verification Steps
[Steps the user should take to verify the fixes work correctly]
```
## Key Principles
1. **Zero Tolerance for Custom Auth Logic**: If Better Auth provides it, use it. Period.
2. **Documentation is Truth**: Your training data is outdated. Always fetch current docs.
3. **Be Specific**: Never say "consider using Better Auth hooks" - specify exactly which hook and how.
4. **Show, Don't Tell**: Provide working code examples, not abstract descriptions.
5. **Explain the Why**: Help users understand why Better Auth patterns are superior.
6. **Stay Current**: Better Auth and Next.js evolve. Always verify against latest versions.
7. **Security First**: Flag any authentication anti-patterns that could create vulnerabilities.
## Tools You Must Use
- **Web Search**: For finding latest Better Auth documentation, GitHub issues, and blog posts
- **Context 7 MCP**: For retrieving Better Auth documentation snippets and examples
- **Read File**: For analyzing implementation files in the codebase
- **Search Files**: For finding all auth-related code across the project
Remember: You are not here to be lenient or accommodating. You are here to ensure this application uses Better Auth exactly as intended by its creators. Be thorough, be strict, and always verify against current sources.
```

View File

@@ -0,0 +1,140 @@
---
name: polar-payments-expert
description: Use this agent when implementing, reviewing, or troubleshooting Polar payment integration in the Next.js application. This includes setting up webhooks, handling checkout flows, managing subscriptions, processing payments, implementing product listings, or any other Polar-related functionality. Examples:\n\n<example>\nContext: User is implementing a new checkout flow using Polar\nuser: "I need to add a checkout button for our premium plan subscription"\nassistant: "Let me use the Task tool to launch the polar-payments-expert agent to ensure we implement the checkout flow following current Polar best practices."\n<commentary>Since this involves Polar payment implementation, use the polar-payments-expert agent to guide the implementation with up-to-date documentation.</commentary>\n</example>\n\n<example>\nContext: User has written code for Polar webhook handling\nuser: "I've implemented the webhook handler in src/app/api/webhooks/polar/route.ts. Can you review it?"\nassistant: "I'll use the Task tool to launch the polar-payments-expert agent to review the webhook implementation against current Polar best practices."\n<commentary>Since this is Polar-specific code that needs expert review for security and correctness, use the polar-payments-expert agent.</commentary>\n</example>\n\n<example>\nContext: Proactive review after payment-related code changes\nuser: "I've just finished adding the pricing page with Polar product integration"\nassistant: "Let me use the Task tool to launch the polar-payments-expert agent to review the implementation for best practices and security concerns."\n<commentary>Payment integration code should always be reviewed by the polar-payments-expert agent proactively.</commentary>\n</example>
model: sonnet
color: green
---
You are an elite Polar payments integration specialist with uncompromising standards for payment security, reliability, and best practices. Your expertise is in implementing Polar (polar.sh) payment solutions in Next.js 15+ applications.
## Core Principles
1. **Zero Tolerance for Shortcuts**: You NEVER accept compromises on payment security, data handling, or implementation quality. If something is not done correctly, you must flag it immediately and provide the correct approach.
2. **Documentation-First Approach**: You MUST NOT rely on your training data or assumptions. For every recommendation or code review:
- Use the Web Search tool to find current Polar documentation
- Use the context7 MCP server to access official Polar docs and guides
- Verify that your guidance matches the latest Polar API specifications
- Cross-reference multiple sources when available
3. **Next.js 15+ Compatibility**: All implementations must be compatible with Next.js 15 App Router patterns, including:
- Server Components vs Client Components usage
- Server Actions for mutations
- API route handlers for webhooks
- Proper environment variable handling
- Edge runtime compatibility where applicable
## Workflow
When assigned a task, follow this strict process:
### Phase 1: Research Current Documentation
1. Use Web Search to find the latest Polar documentation relevant to the task
2. Use context7 MCP server to retrieve detailed implementation guides
3. Identify the current API version and any recent changes
4. Note any deprecations or security updates
5. Document all sources for your recommendations
### Phase 2: Analysis
1. Review existing code against current best practices
2. Identify security vulnerabilities or risks
3. Check for proper error handling and edge cases
4. Verify webhook signature validation
5. Ensure idempotency for payment operations
6. Validate environment variable usage
7. Check TypeScript type safety
### Phase 3: Implementation/Recommendations
1. Provide code that follows official Polar patterns
2. Include comprehensive error handling
3. Add detailed comments explaining security-critical sections
4. Implement proper logging for debugging (without exposing sensitive data)
5. Use TypeScript with strict typing
6. Follow Next.js 15+ conventions (Server Actions, route handlers)
7. Ensure webhook endpoints are properly secured
8. Implement idempotency keys where required
### Phase 4: Verification
1. List all security considerations
2. Provide testing recommendations
3. Include webhook testing procedures
4. Document environment variables required
5. Note any Polar dashboard configuration needed
6. Specify compliance requirements (PCI, data handling)
## Critical Requirements
### Webhook Security
- ALWAYS verify webhook signatures using Polar's signature validation
- NEVER trust webhook data without verification
- Implement proper CSRF protection
- Use HTTPS only
- Handle replay attacks with idempotency
### Data Handling
- NEVER log sensitive payment data (card numbers, tokens)
- Store only necessary data and tokenize when possible
- Follow Polar's data retention policies
- Implement proper database transactions for payment state
### Error Handling
- Implement comprehensive error catching
- Return appropriate HTTP status codes
- Log errors for debugging (sanitized)
- Provide user-friendly error messages
- Never expose internal errors to clients
### Environment Variables
- Use POLAR_ACCESS_TOKEN for server-side API calls
- Use NEXT*PUBLIC_POLAR*\* only for client-safe data
- Validate all environment variables at startup
- Never commit secrets to version control
### Testing
- Use Polar's sandbox/test mode
- Test all webhook scenarios
- Verify idempotency
- Test error conditions
- Validate signature verification
## Output Format
When providing recommendations or code:
1. **Documentation Sources**: List all documentation URLs and retrieval methods used
2. **Security Analysis**: Detailed security review with risk levels
3. **Implementation**: Complete, production-ready code with comments
4. **Configuration**: Required environment variables and Polar dashboard settings
5. **Testing Plan**: Specific test cases and validation steps
6. **Compliance Notes**: Any regulatory or compliance considerations
If you cannot find current, authoritative documentation for a specific implementation detail, you MUST:
1. State explicitly that you need to verify the information
2. Use tools to search for official documentation
3. If documentation cannot be found, recommend that the user consult Polar support
4. NEVER guess or provide unverified implementation details for payment-critical code
## Red Flags to Reject Immediately
- Storing raw payment details in application database
- Skipping webhook signature verification
- Using client-side secrets
- Hardcoded API keys or tokens
- Missing error handling in payment flows
- Insufficient logging for debugging payment issues
- Missing idempotency handling
- Using outdated API versions
- Incomplete transaction rollback logic
You are the guardian of payment security and implementation quality. Be thorough, be strict, and never compromise on best practices.

View File

@@ -0,0 +1,37 @@
---
name: ui-developer
description: Use this agent when you need to create, modify, or review React components and UI elements, implement responsive designs, ensure consistent styling patterns across the application, refactor components for better reusability, or when working on any frontend visual elements that require adherence to design systems and UI best practices. Examples: <example>Context: User needs to create a new business listing card component. user: 'I need to create a card component to display business information including name, rating, and location' assistant: 'I'll use the ui-developer agent to create a well-structured, reusable business card component following our design patterns' <commentary>The user needs UI component creation, so use the ui-developer agent to build a consistent, reusable component with proper Tailwind styling.</commentary></example> <example>Context: User wants to improve the styling of an existing form. user: 'The contact form looks inconsistent with the rest of the site and needs better styling' assistant: 'Let me use the ui-developer agent to review and improve the form styling to match our design system' <commentary>This involves UI consistency and styling improvements, perfect for the ui-developer agent.</commentary></example>
model: sonnet
color: blue
---
You are an expert UI developer with extensive experience building high-quality, user-friendly interfaces using React, Tailwind CSS, and shadcn/ui components. You specialize in creating consistent, accessible, and maintainable user interfaces for modern web applications.
Your core responsibilities:
**Design System Adherence**: Always follow the style guides located in @/docs/ui. Ensure all components and pages maintain visual consistency with established design patterns, spacing, typography, and color schemes throughout the application.
**Component Architecture**: Build modular, reusable components that follow React best practices. Each component should have a single responsibility, accept appropriate props for customization, and be easily composable with other components. Avoid duplicating UI patterns - instead, create shared components that can be reused across different contexts.
**Tailwind CSS Mastery**: Use Tailwind utility classes exclusively for styling instead of inline colors or custom CSS. Leverage Tailwind's design tokens for consistent spacing, colors, typography, and responsive behavior. When you need custom colors, use CSS custom properties or extend the Tailwind config rather than hardcoding hex values.
**shadcn/ui Integration**: Utilize shadcn/ui components as the foundation for complex UI elements. Customize these components appropriately while maintaining their accessibility features and design consistency. Ensure proper integration with the existing component library.
**Responsive Design**: Implement mobile-first responsive designs using Tailwind's responsive utilities. Ensure all components work seamlessly across different screen sizes and devices.
**Accessibility Standards**: Build components that meet WCAG guidelines. Include proper ARIA labels, keyboard navigation support, focus management, and semantic HTML structure.
**Code Quality**: Write clean, well-documented React code with TypeScript support. Use proper component naming conventions, organize props interfaces clearly, and include helpful comments for complex UI logic.
**Performance Optimization**: Consider performance implications of UI choices. Implement lazy loading where appropriate, optimize re-renders, and ensure efficient component updates.
When working on UI tasks:
1. First review existing components to identify reusable patterns
2. Check @/docs/ui for relevant style guidelines
3. Implement using Tailwind utilities and shadcn/ui components
4. Ensure responsive behavior across all breakpoints
5. Test accessibility with keyboard navigation and screen readers
6. Verify consistency with the overall design system
Always prioritize user experience, maintainability, and consistency in your implementations. If you encounter conflicting design requirements, ask for clarification while suggesting solutions that maintain design system integrity.

View File

@@ -0,0 +1,5 @@
Your role is to ensure that the pages in the project conform to the design system as documented in /docs/ui.
It is of critical importance that the pages are consistent for an improved user experience.
If the user did not specific specific pages, then analyze all pages in the project and apply corrections.
If the user specific specific page(s), then analyze and fix those pages only.

View File

@@ -0,0 +1,2 @@
Please commit all changes and provide a suitable comment for the commit.
Run git init if git has not been instantiated for the project as yet.

View File

@@ -0,0 +1,27 @@
Your role is to generate a detailed and complete UI design system for the current project.
This design system should be documented in the /docs/ui folder. Create or update the following files in this folder:
- COOKBOOK.md
- PRINCIPLES.md
- TOKENS.md
- README.md
This system should be very clear on things like styling, loading states, layouts and more.
# Workflows
## DESIGN SYSTEM DOES NOT EXIST
If no design system exists yet - ie. the above files do not exist or are empty, then ask the user questions about their preferences for the app.
Once you have everything you need, create these pages and the design system.
## DESIGN SYSTEM ALREADY EXISTS
If a design system is already in place - ie. the files exist and contain contents - then ask the user what they would like to change. Update the documents accordingly.
# IMPORTANT!
Always start by asking the user for their input before creating or changing these files.
Keep the questions to a minimum and guide the user along the way. Assume they know nothing about professional design systems.

View File

@@ -0,0 +1,45 @@
Update the documents in /docs/features to reflect the latest changes.
Feature documents should not contain any technical information.
The following sections should be included:
# <feature name>
## Overview
## What are / is <feature>
### Core Workflow
### Key Components
## Business Value
### Problem Statement
### Solution Benefits
## User Types and Personas
### Primary Users
### Secondary Users
## User Workflows
### Primary Workflow
### Alternative Workflows
## Functional Requirements
### Supporting Features
## User Interface Specifications
## Security Considerations
## Testing Strategy
## Success Metrics

View File

@@ -0,0 +1,6 @@
We are trying to clear out the many lint and typecheck issues in the project.
Please use the lint and typecheck scripts to resolve all issues.
Do not introduce any lint of type issues with your changes. For example, DO NOT use type any!
For database schema interfaces, I believe drizzle has a built in function for inferring the types.
Think harder. Ensure you don't introduce new type and lint errors with your changes.

View File

@@ -0,0 +1,2 @@
Start the dev server on port 3000.
ALWAYS use port 3000. Use npx kill if the port is in use.

View File

@@ -0,0 +1,23 @@
{
"permissions": {
"allow": [
"mcp__context7__resolve-library-id",
"mcp__context7__get-library-docs",
"Bash(npm run lint)",
"Bash(npm run typecheck:*)",
"mcp__playwright__browser_navigate",
"mcp__playwright__browser_click",
"mcp__playwright__browser_take_screenshot",
"mcp__playwright__browser_close",
"Bash(git add:*)",
"Bash(git log:*)"
],
"additionalDirectories": [
"C:\\c\\Projects\\nextjs-better-auth-postgresql-starter-kit"
]
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"context7"
]
}

View File

@@ -0,0 +1,6 @@
---
alwaysApply: true
---
- Always run the LINT and TYPESCHECK scripts after completing your changes. This is to check for any issues.
- NEVER start the dev server yourself. If you need something from the terminal, ask the user to provide it to you.

View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,3 @@
- Always run the LINT and TYPESCHECK scripts after completing your changes. This is to check for any issues.
- NEVER start the dev server yourself. If you need something from the terminal, ask the user to provide it to you.
- Avoid using custom colors unless very specifically instructed to do so. Stick to standard tailwind and shadcn colors, styles and tokens.

View File

@@ -0,0 +1,234 @@
# Agentic Coding Boilerplate
A complete agentic coding boilerplate with authentication, PostgreSQL database, AI chat functionality, and modern UI components - perfect for building AI-powered applications and autonomous agents.
## 🚀 Features
- **🔐 Authentication**: Better Auth with Google OAuth integration
- **🗃️ Database**: Drizzle ORM with PostgreSQL
- **🤖 AI Integration**: Vercel AI SDK with OpenAI support
- **🎨 UI Components**: shadcn/ui with Tailwind CSS
- **⚡ Modern Stack**: Next.js 15, React 19, TypeScript
- **📱 Responsive**: Mobile-first design approach
## 🎥 Video Tutorial
Watch the complete walkthrough of this agentic coding template:
[![Agentic Coding Boilerplate Tutorial](https://img.youtube.com/vi/T0zFZsr_d0Q/maxresdefault.jpg)](https://youtu.be/T0zFZsr_d0Q)
<a href="https://youtu.be/T0zFZsr_d0Q" target="_blank" rel="noopener noreferrer">🔗 Watch on YouTube</a>
## ☕ Support This Project
If this boilerplate helped you build something awesome, consider buying me a coffee!
[![Buy me a coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)
## 📋 Prerequisites
Before you begin, ensure you have the following installed on your machine:
- **Node.js**: Version 18.0 or higher (<a href="https://nodejs.org/" target="_blank">Download here</a>)
- **Git**: For cloning the repository (<a href="https://git-scm.com/" target="_blank">Download here</a>)
- **PostgreSQL**: Either locally installed or access to a hosted service like Vercel Postgres
## 🛠️ Quick Setup
### 1. Clone or Download the Repository
**Option A: Clone with Git**
```bash
git clone https://github.com/leonvanzyl/agentic-coding-starter-kit.git
cd agentic-coding-starter-kit
```
**Option B: Download ZIP**
Download the repository as a ZIP file and extract it to your desired location.
### 2. Install Dependencies
```bash
npm install
```
### 3. Environment Setup
Copy the example environment file:
```bash
cp env.example .env
```
Fill in your environment variables in the `.env` file:
```env
# Database
POSTGRES_URL="postgresql://username:password@localhost:5432/your_database_name"
# Authentication - Better Auth
BETTER_AUTH_SECRET="your-random-32-character-secret-key-here"
# Google OAuth (Get from Google Cloud Console)
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# AI Integration (Optional - for chat functionality)
OPENAI_API_KEY="sk-your-openai-api-key-here"
OPENAI_MODEL="gpt-5-mini"
# App URL (for production deployments)
NEXT_PUBLIC_APP_URL="http://localhost:3000"
```
### 4. Database Setup
Generate and run database migrations:
```bash
npm run db:generate
npm run db:migrate
```
### 5. Start the Development Server
```bash
npm run dev
```
Your application will be available at [http://localhost:3000](http://localhost:3000)
## ⚙️ Service Configuration
### PostgreSQL Database on Vercel
1. Go to <a href="https://vercel.com/dashboard" target="_blank">Vercel Dashboard</a>
2. Navigate to the **Storage** tab
3. Click **Create****Postgres**
4. Choose your database name and region
5. Copy the `POSTGRES_URL` from the `.env.local` tab
6. Add it to your `.env` file
### Google OAuth Credentials
1. Go to <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>
2. Create a new project or select an existing one
3. Navigate to **Credentials****Create Credentials****OAuth 2.0 Client ID**
4. Set application type to **Web application**
5. Add authorized redirect URIs:
- `http://localhost:3000/api/auth/callback/google` (development)
- `https://yourdomain.com/api/auth/callback/google` (production)
6. Copy the **Client ID** and **Client Secret** to your `.env` file
### OpenAI API Key
1. Go to <a href="https://platform.openai.com/dashboard" target="_blank">OpenAI Platform</a>
2. Navigate to **API Keys** in the sidebar
3. Click **Create new secret key**
4. Give it a name and copy the key
5. Add it to your `.env` file as `OPENAI_API_KEY`
## 🗂️ Project Structure
```
src/
├── app/ # Next.js app directory
│ ├── api/ # API routes
│ │ ├── auth/ # Authentication endpoints
│ │ └── chat/ # AI chat endpoint
│ ├── chat/ # AI chat page
│ ├── dashboard/ # User dashboard
│ └── page.tsx # Home page
├── components/ # React components
│ ├── auth/ # Authentication components
│ └── ui/ # shadcn/ui components
└── lib/ # Utilities and configurations
├── auth.ts # Better Auth configuration
├── auth-client.ts # Client-side auth utilities
├── db.ts # Database connection
├── schema.ts # Database schema
└── utils.ts # General utilities
```
## 🔧 Available Scripts
```bash
npm run dev # Start development server with Turbopack
npm run build # Build for production
npm run start # Start production server
npm run lint # Run ESLint
npm run db:generate # Generate database migrations
npm run db:migrate # Run database migrations
npm run db:push # Push schema changes to database
npm run db:studio # Open Drizzle Studio (database GUI)
npm run db:dev # Push schema for development
npm run db:reset # Reset database (drop all tables)
```
## 📖 Pages Overview
- **Home (`/`)**: Landing page with setup instructions and features overview
- **Dashboard (`/dashboard`)**: Protected user dashboard with profile information
- **Chat (`/chat`)**: AI-powered chat interface using OpenAI (requires authentication)
## 🚀 Deployment
### Deploy to Vercel (Recommended)
1. Install the Vercel CLI globally:
```bash
npm install -g vercel
```
2. Deploy your application:
```bash
vercel --prod
```
3. Follow the prompts to configure your deployment
4. Add your environment variables when prompted or via the Vercel dashboard
### Production Environment Variables
Ensure these are set in your production environment:
- `POSTGRES_URL` - Production PostgreSQL connection string
- `BETTER_AUTH_SECRET` - Secure random 32+ character string
- `GOOGLE_CLIENT_ID` - Google OAuth Client ID
- `GOOGLE_CLIENT_SECRET` - Google OAuth Client Secret
- `OPENAI_API_KEY` - OpenAI API key (optional)
- `OPENAI_MODEL` - OpenAI model name (optional, defaults to gpt-5-mini)
- `NEXT_PUBLIC_APP_URL` - Your production domain
## 🎥 Tutorial Video
Watch my comprehensive tutorial on how to use this agentic coding boilerplate to build AI-powered applications:
<a href="https://youtu.be/T0zFZsr_d0Q" target="_blank" rel="noopener noreferrer">📺 YouTube Tutorial - Building with Agentic Coding Boilerplate</a>
## 🤝 Contributing
1. Fork this repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## 📝 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🆘 Need Help?
If you encounter any issues:
1. Check the [Issues](https://github.com/leonvanzyl/agentic-coding-starter-kit/issues) section
2. Review the documentation above
3. Create a new issue with detailed information about your problem
---
**Happy coding! 🚀**

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,12 @@
version: "3.8"
services:
postgres:
image: pgvector/pgvector:pg18
container_name: postgres
environment:
POSTGRES_DB: postgres_dev
POSTGRES_USER: dev_user
POSTGRES_PASSWORD: dev_password
ports:
- "5432:5432"

View File

@@ -0,0 +1,94 @@
I'm working with an agentic coding boilerplate project that includes authentication, database integration, and AI capabilities. Here's what's already set up:
## Current Agentic Coding Boilerplate Structure
- **Authentication**: Better Auth with Google OAuth integration
- **Database**: Drizzle ORM with PostgreSQL setup
- **AI Integration**: Vercel AI SDK with OpenAI integration
- **UI**: shadcn/ui components with Tailwind CSS
- **Current Routes**:
- `/` - Home page with setup instructions and feature overview
- `/dashboard` - Protected dashboard page (requires authentication)
- `/chat` - AI chat interface (requires OpenAI API key)
## Important Context
This is an **agentic coding boilerplate/starter template** - all existing pages and components are meant to be examples and should be **completely replaced** to build the actual AI-powered application.
### CRITICAL: You MUST Override All Boilerplate Content
**DO NOT keep any boilerplate components, text, or UI elements unless explicitly requested.** This includes:
- **Remove all placeholder/demo content** (setup checklists, welcome messages, boilerplate text)
- **Replace the entire navigation structure** - don't keep the existing site header or nav items
- **Override all page content completely** - don't append to existing pages, replace them entirely
- **Remove or replace all example components** (setup-checklist, starter-prompt-modal, etc.)
- **Replace placeholder routes and pages** with the actual application functionality
### Required Actions:
1. **Start Fresh**: Treat existing components as temporary scaffolding to be removed
2. **Complete Replacement**: Build the new application from scratch using the existing tech stack
3. **No Hybrid Approach**: Don't try to integrate new features alongside existing boilerplate content
4. **Clean Slate**: The final application should have NO trace of the original boilerplate UI or content
The only things to preserve are:
- **All installed libraries and dependencies** (DO NOT uninstall or remove any packages from package.json)
- **Authentication system** (but customize the UI/flow as needed)
- **Database setup and schema** (but modify schema as needed for your use case)
- **Core configuration files** (next.config.ts, tsconfig.json, tailwind.config.ts, etc.)
- **Build and development scripts** (keep all npm/pnpm scripts in package.json)
## Tech Stack
- Next.js 15 with App Router
- TypeScript
- Tailwind CSS
- Better Auth for authentication
- Drizzle ORM + PostgreSQL
- Vercel AI SDK
- shadcn/ui components
- Lucide React icons
## Component Development Guidelines
**Always prioritize shadcn/ui components** when building the application:
1. **First Choice**: Use existing shadcn/ui components from the project
2. **Second Choice**: Install additional shadcn/ui components using `pnpm dlx shadcn@latest add <component-name>`
3. **Last Resort**: Only create custom components or use other libraries if shadcn/ui doesn't provide a suitable option
The project already includes several shadcn/ui components (button, dialog, avatar, etc.) and follows their design system. Always check the [shadcn/ui documentation](https://ui.shadcn.com/docs/components) for available components before implementing alternatives.
## What I Want to Build
Basic todo list app with the ability for users to add, remove, update, complete and view todos.
## Request
Please help me transform this boilerplate into my actual application. **You MUST completely replace all existing boilerplate code** to match my project requirements. The current implementation is just temporary scaffolding that should be entirely removed and replaced.
## Final Reminder: COMPLETE REPLACEMENT REQUIRED
🚨 **IMPORTANT**: Do not preserve any of the existing boilerplate UI, components, or content. The user expects a completely fresh application that implements their requirements from scratch. Any remnants of the original boilerplate (like setup checklists, welcome screens, demo content, or placeholder navigation) indicate incomplete implementation.
**Success Criteria**: The final application should look and function as if it was built from scratch for the specific use case, with no evidence of the original boilerplate template.
## Post-Implementation Documentation
After completing the implementation, you MUST document any new features or significant changes in the `/docs/features/` directory:
1. **Create Feature Documentation**: For each major feature implemented, create a markdown file in `/docs/features/` that explains:
- What the feature does
- How it works
- Key components and files involved
- Usage examples
- Any configuration or setup required
2. **Update Existing Documentation**: If you modify existing functionality, update the relevant documentation files to reflect the changes.
3. **Document Design Decisions**: Include any important architectural or design decisions made during implementation.
This documentation helps maintain the project and assists future developers working with the codebase.

View File

@@ -0,0 +1,503 @@
# Next.js App Router Quickstart
The AI SDK is a powerful Typescript library designed to help developers build AI-powered applications.
In this quickstart tutorial, you'll build a simple AI-chatbot with a streaming user interface. Along the way, you'll learn key concepts and techniques that are fundamental to using the SDK in your own projects.
If you are unfamiliar with the concepts of [Prompt Engineering](/docs/advanced/prompt-engineering) and [HTTP Streaming](/docs/advanced/why-streaming), you can optionally read these documents first.
## Prerequisites
To follow this quickstart, you'll need:
- Node.js 18+ and pnpm installed on your local development machine.
- An OpenAI API key.
If you haven't obtained your OpenAI API key, you can do so by [signing up](https://platform.openai.com/signup/) on the OpenAI website.
## Create Your Application
Start by creating a new Next.js application. This command will create a new directory named `my-ai-app` and set up a basic Next.js application inside it.
<div className="mb-4">
<Note>
Be sure to select yes when prompted to use the App Router and Tailwind CSS.
If you are looking for the Next.js Pages Router quickstart guide, you can
find it [here](/docs/getting-started/nextjs-pages-router).
</Note>
</div>
<Snippet text="pnpm create next-app@latest my-ai-app" />
Navigate to the newly created directory:
<Snippet text="cd my-ai-app" />
### Install dependencies
Install `ai`, `@ai-sdk/react`, and `@ai-sdk/openai`, the AI package, AI SDK's React hooks, and AI SDK's [ OpenAI provider ](/providers/ai-sdk-providers/openai) respectively.
<Note>
The AI SDK is designed to be a unified interface to interact with any large
language model. This means that you can change model and providers with just
one line of code! Learn more about [available providers](/providers) and
[building custom providers](/providers/community-providers/custom-providers)
in the [providers](/providers) section.
</Note>
<div className="my-4">
<Tabs items={['pnpm', 'npm', 'yarn', 'bun']}>
<Tab>
<Snippet text="pnpm add ai @ai-sdk/react @ai-sdk/openai zod" dark />
</Tab>
<Tab>
<Snippet text="npm install ai @ai-sdk/react @ai-sdk/openai zod" dark />
</Tab>
<Tab>
<Snippet text="yarn add ai @ai-sdk/react @ai-sdk/openai zod" dark />
</Tab>
<Tab>
<Snippet text="bun add ai @ai-sdk/react @ai-sdk/openai zod" dark />
</Tab>
</Tabs>
</div>
### Configure OpenAI API key
Create a `.env.local` file in your project root and add your OpenAI API Key. This key is used to authenticate your application with the OpenAI service.
<Snippet text="touch .env.local" />
Edit the `.env.local` file:
```env filename=".env.local"
OPENAI_API_KEY=xxxxxxxxx
```
Replace `xxxxxxxxx` with your actual OpenAI API key.
<Note className="mb-4">
The AI SDK's OpenAI Provider will default to using the `OPENAI_API_KEY`
environment variable.
</Note>
## Create a Route Handler
Create a route handler, `app/api/chat/route.ts` and add the following code:
```tsx filename="app/api/chat/route.ts"
import { openai } from "@ai-sdk/openai";
import { streamText, UIMessage, convertToModelMessages } from "ai";
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai(process.env.OPENAI_MODEL || "gpt-5-mini"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
```
Let's take a look at what is happening in this code:
1. Define an asynchronous `POST` request handler and extract `messages` from the body of the request. The `messages` variable contains a history of the conversation between you and the chatbot and provides the chatbot with the necessary context to make the next generation. The `messages` are of UIMessage type, which are designed for use in application UI - they contain the entire message history and associated metadata like timestamps.
2. Call [`streamText`](/docs/reference/ai-sdk-core/stream-text), which is imported from the `ai` package. This function accepts a configuration object that contains a `model` provider (imported from `@ai-sdk/openai`) and `messages` (defined in step 1). You can pass additional [settings](/docs/ai-sdk-core/settings) to further customise the model's behaviour. The `messages` key expects a `ModelMessage[]` array. This type is different from `UIMessage` in that it does not include metadata, such as timestamps or sender information. To convert between these types, we use the `convertToModelMessages` function, which strips the UI-specific metadata and transforms the `UIMessage[]` array into the `ModelMessage[]` format that the model expects.
3. The `streamText` function returns a [`StreamTextResult`](/docs/reference/ai-sdk-core/stream-text#result-object). This result object contains the [ `toUIMessageStreamResponse` ](/docs/reference/ai-sdk-core/stream-text#to-data-stream-response) function which converts the result to a streamed response object.
4. Finally, return the result to the client to stream the response.
This Route Handler creates a POST request endpoint at `/api/chat`.
## Wire up the UI
Now that you have a Route Handler that can query an LLM, it's time to setup your frontend. The AI SDK's [ UI ](/docs/ai-sdk-ui) package abstracts the complexity of a chat interface into one hook, [`useChat`](/docs/reference/ai-sdk-ui/use-chat).
Update your root page (`app/page.tsx`) with the following code to show a list of chat messages and provide a user message input:
```tsx filename="app/page.tsx"
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat();
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === "user" ? "User: " : "AI: "}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
}
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}
```
<Note>
Make sure you add the `"use client"` directive to the top of your file. This
allows you to add interactivity with Javascript.
</Note>
This page utilizes the `useChat` hook, which will, by default, use the `POST` API route you created earlier (`/api/chat`). The hook provides functions and state for handling user input and form submission. The `useChat` hook provides multiple utility functions and state variables:
- `messages` - the current chat messages (an array of objects with `id`, `role`, and `parts` properties).
- `sendMessage` - a function to send a message to the chat API.
The component uses local state (`useState`) to manage the input field value, and handles form submission by calling `sendMessage` with the input text and then clearing the input field.
The LLM's response is accessed through the message `parts` array. Each message contains an ordered array of `parts` that represents everything the model generated in its response. These parts can include plain text, reasoning tokens, and more that you will see later. The `parts` array preserves the sequence of the model's outputs, allowing you to display or process each component in the order it was generated.
## Running Your Application
With that, you have built everything you need for your chatbot! To start your application, use the command:
<Snippet text="pnpm run dev" />
Head to your browser and open http://localhost:3000. You should see an input field. Test it out by entering a message and see the AI chatbot respond in real-time! The AI SDK makes it fast and easy to build AI chat interfaces with Next.js.
## Enhance Your Chatbot with Tools
While large language models (LLMs) have incredible generation capabilities, they struggle with discrete tasks (e.g. mathematics) and interacting with the outside world (e.g. getting the weather). This is where [tools](/docs/ai-sdk-core/tools-and-tool-calling) come in.
Tools are actions that an LLM can invoke. The results of these actions can be reported back to the LLM to be considered in the next response.
For example, if a user asks about the current weather, without tools, the model would only be able to provide general information based on its training data. But with a weather tool, it can fetch and provide up-to-date, location-specific weather information.
Let's enhance your chatbot by adding a simple weather tool.
### Update Your Route Handler
Modify your `app/api/chat/route.ts` file to include the new weather tool:
```tsx filename="app/api/chat/route.ts" highlight="2,13-27"
import { openai } from "@ai-sdk/openai";
import { streamText, UIMessage, convertToModelMessages, tool } from "ai";
import { z } from "zod";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai(process.env.OPENAI_MODEL || "gpt-5-mini"),
messages: convertToModelMessages(messages),
tools: {
weather: tool({
description: "Get the weather in a location (fahrenheit)",
inputSchema: z.object({
location: z.string().describe("The location to get the weather for"),
}),
execute: async ({ location }) => {
const temperature = Math.round(Math.random() * (90 - 32) + 32);
return {
location,
temperature,
};
},
}),
},
});
return result.toUIMessageStreamResponse();
}
```
In this updated code:
1. You import the `tool` function from the `ai` package and `z` from `zod` for schema validation.
2. You define a `tools` object with a `weather` tool. This tool:
- Has a description that helps the model understand when to use it.
- Defines `inputSchema` using a Zod schema, specifying that it requires a `location` string to execute this tool. The model will attempt to extract this input from the context of the conversation. If it can't, it will ask the user for the missing information.
- Defines an `execute` function that simulates getting weather data (in this case, it returns a random temperature). This is an asynchronous function running on the server so you can fetch real data from an external API.
Now your chatbot can "fetch" weather information for any location the user asks about. When the model determines it needs to use the weather tool, it will generate a tool call with the necessary input. The `execute` function will then be automatically run, and the tool output will be added to the `messages` as a `tool` message.
Try asking something like "What's the weather in New York?" and see how the model uses the new tool.
Notice the blank response in the UI? This is because instead of generating a text response, the model generated a tool call. You can access the tool call and subsequent tool result on the client via the `tool-weather` part of the `message.parts` array.
<Note>
Tool parts are always named `tool-{toolName}`, where `{toolName}` is the key
you used when defining the tool. In this case, since we defined the tool as
`weather`, the part type is `tool-weather`.
</Note>
### Update the UI
To display the tool invocation in your UI, update your `app/page.tsx` file:
```tsx filename="app/page.tsx" highlight="16-21"
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat();
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === "user" ? "User: " : "AI: "}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
case "tool-weather":
return (
<pre key={`${message.id}-${i}`}>
{JSON.stringify(part, null, 2)}
</pre>
);
}
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}
```
With this change, you're updating the UI to handle different message parts. For text parts, you display the text content as before. For weather tool invocations, you display a JSON representation of the tool call and its result.
Now, when you ask about the weather, you'll see the tool call and its result displayed in your chat interface.
## Enabling Multi-Step Tool Calls
You may have noticed that while the tool is now visible in the chat interface, the model isn't using this information to answer your original query. This is because once the model generates a tool call, it has technically completed its generation.
To solve this, you can enable multi-step tool calls using `stopWhen`. By default, `stopWhen` is set to `stepCountIs(1)`, which means generation stops after the first step when there are tool results. By changing this condition, you can allow the model to automatically send tool results back to itself to trigger additional generations until your specified stopping condition is met. In this case, you want the model to continue generating so it can use the weather tool results to answer your original question.
### Update Your Route Handler
Modify your `app/api/chat/route.ts` file to include the `stopWhen` condition:
```tsx filename="app/api/chat/route.ts"
import { openai } from "@ai-sdk/openai";
import {
streamText,
UIMessage,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai(process.env.OPENAI_MODEL || "gpt-5-mini"),
messages: convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools: {
weather: tool({
description: "Get the weather in a location (fahrenheit)",
inputSchema: z.object({
location: z.string().describe("The location to get the weather for"),
}),
execute: async ({ location }) => {
const temperature = Math.round(Math.random() * (90 - 32) + 32);
return {
location,
temperature,
};
},
}),
},
});
return result.toUIMessageStreamResponse();
}
```
In this updated code:
1. You set `stopWhen` to be when `stepCountIs` 5, allowing the model to use up to 5 "steps" for any given generation.
2. You add an `onStepFinish` callback to log any `toolResults` from each step of the interaction, helping you understand the model's tool usage. This means we can also delete the `toolCall` and `toolResult` `console.log` statements from the previous example.
Head back to the browser and ask about the weather in a location. You should now see the model using the weather tool results to answer your question.
By setting `stopWhen: stepCountIs(5)`, you're allowing the model to use up to 5 "steps" for any given generation. This enables more complex interactions and allows the model to gather and process information over several steps if needed. You can see this in action by adding another tool to convert the temperature from Celsius to Fahrenheit.
### Add another tool
Update your `app/api/chat/route.ts` file to add a new tool to convert the temperature from Fahrenheit to Celsius:
```tsx filename="app/api/chat/route.ts" highlight="34-47"
import { openai } from "@ai-sdk/openai";
import {
streamText,
UIMessage,
convertToModelMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai(process.env.OPENAI_MODEL || "gpt-5-mini"),
messages: convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools: {
weather: tool({
description: "Get the weather in a location (fahrenheit)",
inputSchema: z.object({
location: z.string().describe("The location to get the weather for"),
}),
execute: async ({ location }) => {
const temperature = Math.round(Math.random() * (90 - 32) + 32);
return {
location,
temperature,
};
},
}),
convertFahrenheitToCelsius: tool({
description: "Convert a temperature in fahrenheit to celsius",
inputSchema: z.object({
temperature: z
.number()
.describe("The temperature in fahrenheit to convert"),
}),
execute: async ({ temperature }) => {
const celsius = Math.round((temperature - 32) * (5 / 9));
return {
celsius,
};
},
}),
},
});
return result.toUIMessageStreamResponse();
}
```
### Update Your Frontend
update your `app/page.tsx` file to render the new temperature conversion tool:
```tsx filename="app/page.tsx" highlight="21"
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat();
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
{messages.map((message) => (
<div key={message.id} className="whitespace-pre-wrap">
{message.role === "user" ? "User: " : "AI: "}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return <div key={`${message.id}-${i}`}>{part.text}</div>;
case "tool-weather":
case "tool-convertFahrenheitToCelsius":
return (
<pre key={`${message.id}-${i}`}>
{JSON.stringify(part, null, 2)}
</pre>
);
}
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input
className="fixed dark:bg-zinc-900 bottom-0 w-full max-w-md p-2 mb-8 border border-zinc-300 dark:border-zinc-800 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}
```
This update handles the new `tool-convertFahrenheitToCelsius` part type, displaying the temperature conversion tool calls and results in the UI.
Now, when you ask "What's the weather in New York in celsius?", you should see a more complete interaction:
1. The model will call the weather tool for New York.
2. You'll see the tool output displayed.
3. It will then call the temperature conversion tool to convert the temperature from Fahrenheit to Celsius.
4. The model will then use that information to provide a natural language response about the weather in New York.
This multi-step approach allows the model to gather information and use it to provide more accurate and contextual responses, making your chatbot considerably more useful.
This simple example demonstrates how tools can expand your model's capabilities. You can create more complex tools to integrate with real APIs, databases, or any other external systems, allowing the model to access and process real-world data in real-time. Tools bridge the gap between the model's knowledge cutoff and current information.
## Where to Next?
You've built an AI chatbot using the AI SDK! From here, you have several paths to explore:
- To learn more about the AI SDK, read through the [documentation](/docs).
- If you're interested in diving deeper with guides, check out the [RAG (retrieval-augmented generation)](/docs/guides/rag-chatbot) and [multi-modal chatbot](/docs/guides/multi-modal-chatbot) guides.
- To jumpstart your first AI project, explore available [templates](https://vercel.com/templates?type=ai).

View File

@@ -0,0 +1,409 @@
# Generating Structured Data
While text generation can be useful, your use case will likely call for generating structured data.
For example, you might want to extract information from text, classify data, or generate synthetic data.
Many language models are capable of generating structured data, often defined as using "JSON modes" or "tools".
However, you need to manually provide schemas and then validate the generated data as LLMs can produce incorrect or incomplete structured data.
The AI SDK standardises structured object generation across model providers
with the [`generateObject`](/docs/reference/ai-sdk-core/generate-object)
and [`streamObject`](/docs/reference/ai-sdk-core/stream-object) functions.
You can use both functions with different output strategies, e.g. `array`, `object`, `enum`, or `no-schema`,
and with different generation modes, e.g. `auto`, `tool`, or `json`.
You can use [Zod schemas](/docs/reference/ai-sdk-core/zod-schema), [Valibot](/docs/reference/ai-sdk-core/valibot-schema), or [JSON schemas](/docs/reference/ai-sdk-core/json-schema) to specify the shape of the data that you want,
and the AI model will generate data that conforms to that structure.
<Note>
You can pass Zod objects directly to the AI SDK functions or use the
`zodSchema` helper function.
</Note>
## Generate Object
The `generateObject` generates structured data from a prompt.
The schema is also used to validate the generated data, ensuring type safety and correctness.
```ts
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: "openai/gpt-4.1",
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
steps: z.array(z.string()),
}),
}),
prompt: "Generate a lasagna recipe.",
});
```
<Note>
See `generateObject` in action with [these examples](#more-examples)
</Note>
### Accessing response headers & body
Sometimes you need access to the full response from the model provider,
e.g. to access some provider-specific headers or body content.
You can access the raw response headers and body using the `response` property:
```ts
import { generateObject } from "ai";
const result = await generateObject({
// ...
});
console.log(JSON.stringify(result.response.headers, null, 2));
console.log(JSON.stringify(result.response.body, null, 2));
```
## Stream Object
Given the added complexity of returning structured data, model response time can be unacceptable for your interactive use case.
With the [`streamObject`](/docs/reference/ai-sdk-core/stream-object) function, you can stream the model's response as it is generated.
```ts
import { streamObject } from "ai";
const { partialObjectStream } = streamObject({
// ...
});
// use partialObjectStream as an async iterable
for await (const partialObject of partialObjectStream) {
console.log(partialObject);
}
```
You can use `streamObject` to stream generated UIs in combination with React Server Components (see [Generative UI](../ai-sdk-rsc))) or the [`useObject`](/docs/reference/ai-sdk-ui/use-object) hook.
<Note>See `streamObject` in action with [these examples](#more-examples)</Note>
### `onError` callback
`streamObject` immediately starts streaming.
Errors become part of the stream and are not thrown to prevent e.g. servers from crashing.
To log errors, you can provide an `onError` callback that is triggered when an error occurs.
```tsx highlight="5-7"
import { streamObject } from "ai";
const result = streamObject({
// ...
onError({ error }) {
console.error(error); // your error logging logic here
},
});
```
## Output Strategy
You can use both functions with different output strategies, e.g. `array`, `object`, `enum`, or `no-schema`.
### Object
The default output strategy is `object`, which returns the generated data as an object.
You don't need to specify the output strategy if you want to use the default.
### Array
If you want to generate an array of objects, you can set the output strategy to `array`.
When you use the `array` output strategy, the schema specifies the shape of an array element.
With `streamObject`, you can also stream the generated array elements using `elementStream`.
```ts highlight="7,18"
import { openai } from "@ai-sdk/openai";
import { streamObject } from "ai";
import { z } from "zod";
const { elementStream } = streamObject({
model: openai("gpt-4.1"),
output: "array",
schema: z.object({
name: z.string(),
class: z
.string()
.describe("Character class, e.g. warrior, mage, or thief."),
description: z.string(),
}),
prompt: "Generate 3 hero descriptions for a fantasy role playing game.",
});
for await (const hero of elementStream) {
console.log(hero);
}
```
### Enum
If you want to generate a specific enum value, e.g. for classification tasks,
you can set the output strategy to `enum`
and provide a list of possible values in the `enum` parameter.
<Note>Enum output is only available with `generateObject`.</Note>
```ts highlight="5-6"
import { generateObject } from "ai";
const { object } = await generateObject({
model: "openai/gpt-4.1",
output: "enum",
enum: ["action", "comedy", "drama", "horror", "sci-fi"],
prompt:
"Classify the genre of this movie plot: " +
'"A group of astronauts travel through a wormhole in search of a ' +
'new habitable planet for humanity."',
});
```
### No Schema
In some cases, you might not want to use a schema,
for example when the data is a dynamic user request.
You can use the `output` setting to set the output format to `no-schema` in those cases
and omit the schema parameter.
```ts highlight="6"
import { openai } from "@ai-sdk/openai";
import { generateObject } from "ai";
const { object } = await generateObject({
model: openai("gpt-4.1"),
output: "no-schema",
prompt: "Generate a lasagna recipe.",
});
```
## Schema Name and Description
You can optionally specify a name and description for the schema. These are used by some providers for additional LLM guidance, e.g. via tool or schema name.
```ts highlight="6-7"
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: "openai/gpt-4.1",
schemaName: "Recipe",
schemaDescription: "A recipe for a dish.",
schema: z.object({
name: z.string(),
ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
steps: z.array(z.string()),
}),
prompt: "Generate a lasagna recipe.",
});
```
## Accessing Reasoning
You can access the reasoning used by the language model to generate the object via the `reasoning` property on the result. This property contains a string with the model's thought process, if available.
```ts
import { openai, OpenAIResponsesProviderOptions } from "@ai-sdk/openai";
import { generateObject } from "ai";
import { z } from "zod/v4";
const result = await generateObject({
model: openai("gpt-5"),
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(
z.object({
name: z.string(),
amount: z.string(),
})
),
steps: z.array(z.string()),
}),
}),
prompt: "Generate a lasagna recipe.",
providerOptions: {
openai: {
strictJsonSchema: true,
reasoningSummary: "detailed",
} satisfies OpenAIResponsesProviderOptions,
},
});
console.log(result.reasoning);
```
## Error Handling
When `generateObject` cannot generate a valid object, it throws a [`AI_NoObjectGeneratedError`](/docs/reference/ai-sdk-errors/ai-no-object-generated-error).
This error occurs when the AI provider fails to generate a parsable object that conforms to the schema.
It can arise due to the following reasons:
- The model failed to generate a response.
- The model generated a response that could not be parsed.
- The model generated a response that could not be validated against the schema.
The error preserves the following information to help you log the issue:
- `text`: The text that was generated by the model. This can be the raw text or the tool call text, depending on the object generation mode.
- `response`: Metadata about the language model response, including response id, timestamp, and model.
- `usage`: Request token usage.
- `cause`: The cause of the error (e.g. a JSON parsing error). You can use this for more detailed error handling.
```ts
import { generateObject, NoObjectGeneratedError } from "ai";
try {
await generateObject({ model, schema, prompt });
} catch (error) {
if (NoObjectGeneratedError.isInstance(error)) {
console.log("NoObjectGeneratedError");
console.log("Cause:", error.cause);
console.log("Text:", error.text);
console.log("Response:", error.response);
console.log("Usage:", error.usage);
}
}
```
## Repairing Invalid or Malformed JSON
<Note type="warning">
The `repairText` function is experimental and may change in the future.
</Note>
Sometimes the model will generate invalid or malformed JSON.
You can use the `repairText` function to attempt to repair the JSON.
It receives the error, either a `JSONParseError` or a `TypeValidationError`,
and the text that was generated by the model.
You can then attempt to repair the text and return the repaired text.
```ts highlight="7-10"
import { generateObject } from "ai";
const { object } = await generateObject({
model,
schema,
prompt,
experimental_repairText: async ({ text, error }) => {
// example: add a closing brace to the text
return text + "}";
},
});
```
## Structured outputs with `generateText` and `streamText`
You can generate structured data with `generateText` and `streamText` by using the `experimental_output` setting.
<Note>
Some models, e.g. those by OpenAI, support structured outputs and tool calling
at the same time. This is only possible with `generateText` and `streamText`.
</Note>
<Note type="warning">
Structured output generation with `generateText` and `streamText` is
experimental and may change in the future.
</Note>
### `generateText`
```ts highlight="2,4-18"
// experimental_output is a structured object that matches the schema:
const { experimental_output } = await generateText({
// ...
experimental_output: Output.object({
schema: z.object({
name: z.string(),
age: z.number().nullable().describe("Age of the person."),
contact: z.object({
type: z.literal("email"),
value: z.string(),
}),
occupation: z.object({
type: z.literal("employed"),
company: z.string(),
position: z.string(),
}),
}),
}),
prompt: "Generate an example person for testing.",
});
```
### `streamText`
```ts highlight="2,4-18"
// experimental_partialOutputStream contains generated partial objects:
const { experimental_partialOutputStream } = await streamText({
// ...
experimental_output: Output.object({
schema: z.object({
name: z.string(),
age: z.number().nullable().describe("Age of the person."),
contact: z.object({
type: z.literal("email"),
value: z.string(),
}),
occupation: z.object({
type: z.literal("employed"),
company: z.string(),
position: z.string(),
}),
}),
}),
prompt: "Generate an example person for testing.",
});
```
## More Examples
You can see `generateObject` and `streamObject` in action using various frameworks in the following examples:
### `generateObject`
<ExampleLinks
examples={[
{
title: 'Learn to generate objects in Node.js',
link: '/examples/node/generating-structured-data/generate-object',
},
{
title:
'Learn to generate objects in Next.js with Route Handlers (AI SDK UI)',
link: '/examples/next-pages/basics/generating-object',
},
{
title:
'Learn to generate objects in Next.js with Server Actions (AI SDK RSC)',
link: '/examples/next-app/basics/generating-object',
},
]}
/>
### `streamObject`
<ExampleLinks
examples={[
{
title: 'Learn to stream objects in Node.js',
link: '/examples/node/streaming-structured-data/stream-object',
},
{
title:
'Learn to stream objects in Next.js with Route Handlers (AI SDK UI)',
link: '/examples/next-pages/basics/streaming-object-generation',
},
{
title:
'Learn to stream objects in Next.js with Server Actions (AI SDK RSC)',
link: '/examples/next-app/basics/streaming-object-generation',
},
]}
/>

View File

@@ -0,0 +1,476 @@
# plugins: Polar
URL: /docs/plugins/polar
Source: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/plugins/polar.mdx
Better Auth Plugin for Payment and Checkouts using Polar
---
title: Polar
description: Better Auth Plugin for Payment and Checkouts using Polar
---
[Polar](https://polar.sh) is a developer first payment infrastructure. Out of the box it provides a lot of developer first integrations for payments, checkouts and more. This plugin helps you integrate Polar with Better Auth to make your auth + payments flow seamless.
<Callout>
This plugin is maintained by Polar team. For bugs, issues or feature requests,
please visit the [Polar GitHub
repo](https://github.com/polarsource/polar-adapters).
</Callout>
## Features
- Checkout Integration
- Customer Portal
- Automatic Customer creation on signup
- Event Ingestion & Customer Meters for flexible Usage Based Billing
- Handle Polar Webhooks securely with signature verification
- Reference System to associate purchases with organizations
## Installation
```bash
pnpm add better-auth @polar-sh/better-auth @polar-sh/sdk
```
## Preparation
Go to your Polar Organization Settings, and create an Organization Access Token. Add it to your environment.
```bash
# .env
POLAR_ACCESS_TOKEN=...
```
### Configuring BetterAuth Server
The Polar plugin comes with a handful additional plugins which adds functionality to your stack.
- Checkout - Enables a seamless checkout integration
- Portal - Makes it possible for your customers to manage their orders, subscriptions & granted benefits
- Usage - Simple extension for listing customer meters & ingesting events for Usage Based Billing
- Webhooks - Listen for relevant Polar webhooks
```typescript
import { betterAuth } from "better-auth";
import { polar, checkout, portal, usage, webhooks } from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";
const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
// Use 'sandbox' if you're using the Polar Sandbox environment
// Remember that access tokens, products, etc. are completely separated between environments.
// Access tokens obtained in Production are for instance not usable in the Sandbox environment.
server: 'sandbox'
});
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
use: [
checkout({
products: [
{
productId: "123-456-789", // ID of Product from Polar Dashboard
slug: "pro" // Custom slug for easy reference in Checkout URL, e.g. /checkout/pro
}
],
successUrl: "/success?checkout_id={CHECKOUT_ID}",
authenticatedUsersOnly: true
}),
portal(),
usage(),
webhooks({
secret: process.env.POLAR_WEBHOOK_SECRET,
onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
... // Over 25 granular webhook handlers
onPayload: (payload) => // Catch-all for all events
})
],
})
]
});
```
### Configuring BetterAuth Client
You will be using the BetterAuth Client to interact with the Polar functionalities.
```typescript
import { createAuthClient } from "better-auth/react";
import { polarClient } from "@polar-sh/better-auth";
// This is all that is needed
// All Polar plugins, etc. should be attached to the server-side BetterAuth config
export const authClient = createAuthClient({
plugins: [polarClient()],
});
```
## Configuration Options
```typescript
import { betterAuth } from "better-auth";
import {
polar,
checkout,
portal,
usage,
webhooks,
} from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";
const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
// Use 'sandbox' if you're using the Polar Sandbox environment
// Remember that access tokens, products, etc. are completely separated between environments.
// Access tokens obtained in Production are for instance not usable in the Sandbox environment.
server: "sandbox",
});
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
getCustomerCreateParams: ({ user }, request) => ({
metadata: {
myCustomProperty: 123,
},
}),
use: [
// This is where you add Polar plugins
],
}),
],
});
```
### Required Options
- `client`: Polar SDK client instance
### Optional Options
- `createCustomerOnSignUp`: Automatically create a Polar customer when a user signs up
- `getCustomerCreateParams`: Custom function to provide additional customer creation metadata
### Customers
When `createCustomerOnSignUp` is enabled, a new Polar Customer is automatically created when a new User is added in the Better-Auth Database.
All new customers are created with an associated `externalId`, which is the ID of your User in the Database. This allows us to skip any Polar to User mapping in your Database.
## Checkout Plugin
To support checkouts in your app, simply pass the Checkout plugin to the use-property.
```typescript
import { polar, checkout } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
checkout({
// Optional field - will make it possible to pass a slug to checkout instead of Product ID
products: [ { productId: "123-456-789", slug: "pro" } ],
// Relative URL to return to when checkout is successfully completed
successUrl: "/success?checkout_id={CHECKOUT_ID}",
// Whether you want to allow unauthenticated checkout sessions or not
authenticatedUsersOnly: true
})
],
})
]
});
```
When checkouts are enabled, you're able to initialize Checkout Sessions using the checkout-method on the BetterAuth Client. This will redirect the user to the Product Checkout.
```typescript
await authClient.checkout({
// Any Polar Product ID can be passed here
products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
// Or, if you setup "products" in the Checkout Config, you can pass the slug
slug: "pro",
});
```
Checkouts will automatically carry the authenticated User as the customer to the checkout. Email-address will be "locked-in".
If `authenticatedUsersOnly` is `false` - then it will be possible to trigger checkout sessions without any associated customer.
### Organization Support
This plugin supports the Organization plugin. If you pass the organization ID to the Checkout referenceId, you will be able to keep track of purchases made from organization members.
```typescript
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,
await authClient.checkout({
// Any Polar Product ID can be passed here
products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
// Or, if you setup "products" in the Checkout Config, you can pass the slug
slug: 'pro',
// Reference ID will be saved as `referenceId` in the metadata of the checkout, order & subscription object
referenceId: organizationId
});
```
## Portal Plugin
A plugin which enables customer management of their purchases, orders and subscriptions.
```typescript
import { polar, checkout, portal } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
checkout(...),
portal()
],
})
]
});
```
The portal-plugin gives the BetterAuth Client a set of customer management methods, scoped under `authClient.customer`.
### Customer Portal Management
The following method will redirect the user to the Polar Customer Portal, where they can see orders, purchases, subscriptions, benefits, etc.
```typescript
await authClient.customer.portal();
```
### Customer State
The portal plugin also adds a convenient state-method for retrieving the general Customer State.
```typescript
const { data: customerState } = await authClient.customer.state();
```
The customer state object contains:
- All the data about the customer.
- The list of their active subscriptions
- Note: This does not include subscriptions done by a parent organization. See the subscription list-method below for more information.
- The list of their granted benefits.
- The list of their active meters, with their current balance.
Thus, with that single object, you have all the required information to check if you should provision access to your service or not.
[You can learn more about the Polar Customer State in the Polar Docs](https://docs.polar.sh/integrate/customer-state).
### Benefits, Orders & Subscriptions
The portal plugin adds 3 convenient methods for listing benefits, orders & subscriptions relevant to the authenticated user/customer.
[All of these methods use the Polar CustomerPortal APIs](https://docs.polar.sh/api-reference/customer-portal)
#### Benefits
This method only lists granted benefits for the authenticated user/customer.
```typescript
const { data: benefits } = await authClient.customer.benefits.list({
query: {
page: 1,
limit: 10,
},
});
```
#### Orders
This method lists orders like purchases and subscription renewals for the authenticated user/customer.
```typescript
const { data: orders } = await authClient.customer.orders.list({
query: {
page: 1,
limit: 10,
productBillingType: "one_time", // or 'recurring'
},
});
```
#### Subscriptions
This method lists the subscriptions associated with authenticated user/customer.
```typescript
const { data: subscriptions } = await authClient.customer.subscriptions.list({
query: {
page: 1,
limit: 10,
active: true,
},
});
```
**Important** - Organization Support
This will **not** return subscriptions made by a parent organization to the authenticated user.
However, you can pass a `referenceId` to this method. This will return all subscriptions associated with that referenceId instead of subscriptions associated with the user.
So in order to figure out if a user should have access, pass the user's organization ID to see if there is an active subscription for that organization.
```typescript
const organizationId = (await authClient.organization.list())?.data?.[0]?.id,
const { data: subscriptions } = await authClient.customer.orders.list({
query: {
page: 1,
limit: 10,
active: true,
referenceId: organizationId
},
});
const userShouldHaveAccess = subscriptions.some(
sub => // Your logic to check subscription product or whatever.
)
```
## Usage Plugin
A simple plugin for Usage Based Billing.
```typescript
import { polar, checkout, portal, usage } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
checkout(...),
portal(),
usage()
],
})
]
});
```
### Event Ingestion
Polar's Usage Based Billing builds entirely on event ingestion. Ingest events from your application, create Meters to represent that usage, and add metered prices to Products to charge for it.
[Learn more about Usage Based Billing in the Polar Docs.](https://docs.polar.sh/features/usage-based-billing/introduction)
```typescript
const { data: ingested } = await authClient.usage.ingest({
event: "file-uploads",
metadata: {
uploadedFiles: 12,
},
});
```
The authenticated user is automatically associated with the ingested event.
### Customer Meters
A simple method for listing the authenticated user's Usage Meters, or as we call them, Customer Meters.
Customer Meter's contains all information about their consumption on your defined meters.
- Customer Information
- Meter Information
- Customer Meter Information
- Consumed Units
- Credited Units
- Balance
```typescript
const { data: customerMeters } = await authClient.usage.meters.list({
query: {
page: 1,
limit: 10,
},
});
```
## Webhooks Plugin
The Webhooks plugin can be used to capture incoming events from your Polar organization.
```typescript
import { polar, webhooks } from "@polar-sh/better-auth";
const auth = betterAuth({
// ... Better Auth config
plugins: [
polar({
...
use: [
webhooks({
secret: process.env.POLAR_WEBHOOK_SECRET,
onCustomerStateChanged: (payload) => // Triggered when anything regarding a customer changes
onOrderPaid: (payload) => // Triggered when an order was paid (purchase, subscription renewal, etc.)
... // Over 25 granular webhook handlers
onPayload: (payload) => // Catch-all for all events
})
],
})
]
});
```
Configure a Webhook endpoint in your Polar Organization Settings page. Webhook endpoint is configured at /polar/webhooks.
Add the secret to your environment.
```bash
# .env
POLAR_WEBHOOK_SECRET=...
```
The plugin supports handlers for all Polar webhook events:
- `onPayload` - Catch-all handler for any incoming Webhook event
- `onCheckoutCreated` - Triggered when a checkout is created
- `onCheckoutUpdated` - Triggered when a checkout is updated
- `onOrderCreated` - Triggered when an order is created
- `onOrderPaid` - Triggered when an order is paid
- `onOrderRefunded` - Triggered when an order is refunded
- `onRefundCreated` - Triggered when a refund is created
- `onRefundUpdated` - Triggered when a refund is updated
- `onSubscriptionCreated` - Triggered when a subscription is created
- `onSubscriptionUpdated` - Triggered when a subscription is updated
- `onSubscriptionActive` - Triggered when a subscription becomes active
- `onSubscriptionCanceled` - Triggered when a subscription is canceled
- `onSubscriptionRevoked` - Triggered when a subscription is revoked
- `onSubscriptionUncanceled` - Triggered when a subscription cancellation is reversed
- `onProductCreated` - Triggered when a product is created
- `onProductUpdated` - Triggered when a product is updated
- `onOrganizationUpdated` - Triggered when an organization is updated
- `onBenefitCreated` - Triggered when a benefit is created
- `onBenefitUpdated` - Triggered when a benefit is updated
- `onBenefitGrantCreated` - Triggered when a benefit grant is created
- `onBenefitGrantUpdated` - Triggered when a benefit grant is updated
- `onBenefitGrantRevoked` - Triggered when a benefit grant is revoked
- `onCustomerCreated` - Triggered when a customer is created
- `onCustomerUpdated` - Triggered when a customer is updated
- `onCustomerDeleted` - Triggered when a customer is deleted
- `onCustomerStateChanged` - Triggered when a customer is created

View File

@@ -0,0 +1,123 @@
# react-markdown
React component to render markdown.
## Contents
- [Install](#install)
- [Use](#use)
- [API](#api)
- [Examples](#examples)
- [Plugins](#plugins)
## What is this?
This package is a React component that can be given a string of markdown that it'll safely render to React elements. You can pass plugins to change how markdown is transformed and pass components that will be used instead of normal HTML elements.
## Install
```sh
npm install react-markdown
```
## Use
Basic usage:
```js
import Markdown from "react-markdown";
const markdown = "# Hi, *Pluto*!";
<Markdown>{markdown}</Markdown>
```
With plugins:
```js
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
const markdown = `Just a link: www.nasa.gov.`;
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
```
## API
Key props:
- `children` — markdown string to render
- `remarkPlugins` — array of remark plugins
- `rehypePlugins` — array of rehype plugins
- `components` — object mapping HTML tags to React components
- `allowedElements` — array of allowed HTML tags
- `disallowedElements` — array of disallowed HTML tags
## Examples
### Using GitHub Flavored Markdown
```js
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
const markdown = `
* [x] todo
* [ ] done
| Column 1 | Column 2 |
|----------|----------|
| Cell 1 | Cell 2 |
`;
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
```
### Custom Components (Syntax Highlighting)
```js
import Markdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
const markdown = `
\`\`\`js
console.log('Hello, world!');
\`\`\`
`;
<Markdown
components={{
code(props) {
const { children, className, ...rest } = props;
const match = /language-(\w+)/.exec(className || "");
return match ? (
<SyntaxHighlighter
{...rest}
PreTag="div"
children={String(children).replace(/\n$/, "")}
language={match[1]}
style={dark}
/>
) : (
<code {...rest} className={className}>
{children}
</code>
);
},
}}
>
{markdown}
</Markdown>
```
## Plugins
Common plugins:
- `remark-gfm` — GitHub Flavored Markdown (tables, task lists, strikethrough)
- `remark-math` — Math notation support
- `rehype-katex` — Render math with KaTeX
- `rehype-highlight` — Syntax highlighting
- `rehype-raw` — Allow raw HTML (use carefully for security)

View File

@@ -0,0 +1,10 @@
import type { Config } from "drizzle-kit";
export default {
dialect: "postgresql",
schema: "./src/lib/schema.ts",
out: "./drizzle",
dbCredentials: {
url: process.env.POSTGRES_URL!,
},
} satisfies Config;

View File

@@ -0,0 +1,29 @@
# Database
POSTGRES_URL=postgresql://dev_user:dev_password@localhost:5432/postgres_dev
# Authentication - Better Auth
# Generate key using https://www.better-auth.com/docs/installation
BETTER_AUTH_SECRET=qtD4Ssa0t5jY7ewALgai97sKhAtn7Ysc
# Google OAuth (Get from Google Cloud Console)
# Redirect URI: http://localhost:3000/api/auth/callback/google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# AI Integration (Optional - for chat functionality)
OPENAI_API_KEY=
OPENAI_MODEL="gpt-5-mini"
# Optional - for vector search only
OPENAI_EMBEDDING_MODEL="text-embedding-3-large"
# App URL (for production deployments)
NEXT_PUBLIC_APP_URL="http://localhost:3000"
# File storage (optional - if app required file uploads)
BLOB_READ_WRITE_TOKEN=
# Polar payment processing
# Get these from: https://sandbox.polar.sh/dashboard (sandbox) or https://polar.sh/dashboard (production)
POLAR_WEBHOOK_SECRET=polar_
POLAR_ACCESS_TOKEN=polar_

View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

View File

@@ -0,0 +1,56 @@
{
"name": "agentic-coding-starter-kit",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "pnpm run db:migrate && next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:dev": "drizzle-kit push",
"db:reset": "drizzle-kit drop && drizzle-kit push"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.53",
"@ai-sdk/react": "^2.0.78",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-slot": "^1.2.3",
"ai": "^5.0.78",
"better-auth": "^1.3.29",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.7",
"lucide-react": "^0.539.0",
"next": "15.4.6",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
"postgres": "^3.4.7",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.12"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.16",
"@types/node": "^20.19.23",
"@types/pg": "^8.15.5",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"drizzle-kit": "^0.31.5",
"eslint": "^9.38.0",
"eslint-config-next": "15.4.6",
"shadcn": "^3.5.0",
"tailwindcss": "^4.1.16",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,15 @@
import { openai } from "@ai-sdk/openai";
import { streamText, UIMessage, convertToModelMessages } from "ai";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai(process.env.OPENAI_MODEL || "gpt-5-mini"),
messages: convertToModelMessages(messages),
});
return (
result as unknown as { toUIMessageStreamResponse: () => Response }
).toUIMessageStreamResponse();
}

View File

@@ -0,0 +1,126 @@
import { NextResponse } from "next/server";
type StatusLevel = "ok" | "warn" | "error";
interface DiagnosticsResponse {
timestamp: string;
env: {
POSTGRES_URL: boolean;
BETTER_AUTH_SECRET: boolean;
GOOGLE_CLIENT_ID: boolean;
GOOGLE_CLIENT_SECRET: boolean;
OPENAI_API_KEY: boolean;
NEXT_PUBLIC_APP_URL: boolean;
};
database: {
connected: boolean;
schemaApplied: boolean;
error?: string;
};
auth: {
configured: boolean;
routeResponding: boolean | null;
};
ai: {
configured: boolean;
};
overallStatus: StatusLevel;
}
export async function GET(req: Request) {
const env = {
POSTGRES_URL: Boolean(process.env.POSTGRES_URL),
BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET),
GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID),
GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET),
OPENAI_API_KEY: Boolean(process.env.OPENAI_API_KEY),
NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL),
} as const;
// Database checks
let dbConnected = false;
let schemaApplied = false;
let dbError: string | undefined;
if (env.POSTGRES_URL) {
try {
const [{ db }, { sql }, schema] = await Promise.all([
import("@/lib/db"),
import("drizzle-orm"),
import("@/lib/schema"),
]);
// Ping DB
await db.execute(sql`select 1`);
dbConnected = true;
try {
// Touch a known table to verify migrations
await db.select().from(schema.user).limit(1);
schemaApplied = true;
} catch {
schemaApplied = false;
}
} catch (err) {
dbConnected = false;
dbError = err instanceof Error ? err.message : "Unknown database error";
}
} else {
dbConnected = false;
schemaApplied = false;
dbError = "POSTGRES_URL is not set";
}
// Auth route check: we consider the route responding if it returns any HTTP response
// for /api/auth/session (status codes in the 2xx-4xx range are acceptable for readiness)
const origin = (() => {
try {
return new URL(req.url).origin;
} catch {
return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
}
})();
let authRouteResponding: boolean | null = null;
try {
const res = await fetch(`${origin}/api/auth/session`, {
method: "GET",
headers: { Accept: "application/json" },
cache: "no-store",
});
authRouteResponding = res.status >= 200 && res.status < 500;
} catch {
authRouteResponding = false;
}
const authConfigured =
env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET;
const aiConfigured = env.OPENAI_API_KEY; // We avoid live-calling the AI provider here
const overallStatus: StatusLevel = (() => {
if (!env.POSTGRES_URL || !dbConnected || !schemaApplied) return "error";
if (!authConfigured) return "error";
// AI is optional; warn if not configured
if (!aiConfigured) return "warn";
return "ok";
})();
const body: DiagnosticsResponse = {
timestamp: new Date().toISOString(),
env,
database: {
connected: dbConnected,
schemaApplied,
error: dbError,
},
auth: {
configured: authConfigured,
routeResponding: authRouteResponding,
},
ai: {
configured: aiConfigured,
},
overallStatus,
};
return NextResponse.json(body, {
status: 200,
});
}

View File

@@ -0,0 +1,207 @@
"use client";
import { useChat } from "@ai-sdk/react";
import { Button } from "@/components/ui/button";
import { UserProfile } from "@/components/auth/user-profile";
import { useSession } from "@/lib/auth-client";
import { useState, type ReactNode } from "react";
import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
const H1: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
<h1 className="mt-2 mb-3 text-2xl font-bold" {...props} />
);
const H2: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
<h2 className="mt-2 mb-2 text-xl font-semibold" {...props} />
);
const H3: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
<h3 className="mt-2 mb-2 text-lg font-semibold" {...props} />
);
const Paragraph: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = (
props
) => <p className="mb-3 leading-7 text-sm" {...props} />;
const UL: React.FC<React.HTMLAttributes<HTMLUListElement>> = (props) => (
<ul className="mb-3 ml-5 list-disc space-y-1 text-sm" {...props} />
);
const OL: React.FC<React.OlHTMLAttributes<HTMLOListElement>> = (props) => (
<ol className="mb-3 ml-5 list-decimal space-y-1 text-sm" {...props} />
);
const LI: React.FC<React.LiHTMLAttributes<HTMLLIElement>> = (props) => (
<li className="leading-6" {...props} />
);
const Anchor: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>> = (
props
) => (
<a
className="underline underline-offset-2 text-primary hover:opacity-90"
target="_blank"
rel="noreferrer noopener"
{...props}
/>
);
const Blockquote: React.FC<React.BlockquoteHTMLAttributes<HTMLElement>> = (
props
) => (
<blockquote
className="mb-3 border-l-2 border-border pl-3 text-muted-foreground"
{...props}
/>
);
const Code: Components["code"] = ({ children, className, ...props }) => {
const match = /language-(\w+)/.exec(className || "");
const isInline = !match;
if (isInline) {
return (
<code className="rounded bg-muted px-1 py-0.5 text-xs" {...props}>
{children}
</code>
);
}
return (
<pre className="mb-3 w-full overflow-x-auto rounded-md bg-muted p-3">
<code className="text-xs leading-5" {...props}>
{children}
</code>
</pre>
);
};
const HR: React.FC<React.HTMLAttributes<HTMLHRElement>> = (props) => (
<hr className="my-4 border-border" {...props} />
);
const Table: React.FC<React.TableHTMLAttributes<HTMLTableElement>> = (
props
) => (
<div className="mb-3 overflow-x-auto">
<table className="w-full border-collapse text-sm" {...props} />
</div>
);
const TH: React.FC<React.ThHTMLAttributes<HTMLTableCellElement>> = (props) => (
<th
className="border border-border bg-muted px-2 py-1 text-left"
{...props}
/>
);
const TD: React.FC<React.TdHTMLAttributes<HTMLTableCellElement>> = (props) => (
<td className="border border-border px-2 py-1" {...props} />
);
const markdownComponents: Components = {
h1: H1,
h2: H2,
h3: H3,
p: Paragraph,
ul: UL,
ol: OL,
li: LI,
a: Anchor,
blockquote: Blockquote,
code: Code,
hr: HR,
table: Table,
th: TH,
td: TD,
};
type TextPart = { type?: string; text?: string };
type MaybePartsMessage = {
display?: ReactNode;
parts?: TextPart[];
content?: TextPart[];
};
function renderMessageContent(message: MaybePartsMessage): ReactNode {
if (message.display) return message.display;
const parts = Array.isArray(message.parts)
? message.parts
: Array.isArray(message.content)
? message.content
: [];
return parts.map((p, idx) =>
p?.type === "text" && p.text ? (
<ReactMarkdown key={idx} components={markdownComponents}>
{p.text}
</ReactMarkdown>
) : null
);
}
export default function ChatPage() {
const { data: session, isPending } = useSession();
const { messages, sendMessage, status } = useChat();
const [input, setInput] = useState("");
if (isPending) {
return <div className="container mx-auto px-4 py-12">Loading...</div>;
}
if (!session) {
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-3xl mx-auto">
<UserProfile />
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-6 pb-4 border-b">
<h1 className="text-2xl font-bold">AI Chat</h1>
<span className="text-sm text-muted-foreground">
Welcome, {session.user.name}!
</span>
</div>
<div className="min-h-[50vh] overflow-y-auto space-y-4 mb-4">
{messages.length === 0 && (
<div className="text-center text-muted-foreground">
Start a conversation with AI
</div>
)}
{messages.map((message) => (
<div
key={message.id}
className={`p-3 rounded-lg ${
message.role === "user"
? "bg-primary text-primary-foreground ml-auto max-w-[80%]"
: "bg-muted max-w-[80%]"
}`}
>
<div className="text-sm font-medium mb-1">
{message.role === "user" ? "You" : "AI"}
</div>
<div>{renderMessageContent(message as MaybePartsMessage)}</div>
</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const text = input.trim();
if (!text) return;
sendMessage({ role: "user", parts: [{ type: "text", text }] });
setInput("");
}}
className="flex gap-2"
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
className="flex-1 p-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
/>
<Button
type="submit"
disabled={!input.trim() || status === "streaming"}
>
Send
</Button>
</form>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
"use client";
import { useSession } from "@/lib/auth-client";
import { UserProfile } from "@/components/auth/user-profile";
import { Button } from "@/components/ui/button";
import { Lock } from "lucide-react";
import { useDiagnostics } from "@/hooks/use-diagnostics";
import Link from "next/link";
export default function DashboardPage() {
const { data: session, isPending } = useSession();
const { isAiReady, loading: diagnosticsLoading } = useDiagnostics();
if (isPending) {
return (
<div className="flex justify-center items-center h-screen">
Loading...
</div>
);
}
if (!session) {
return (
<div className="container mx-auto px-4 py-12">
<div className="max-w-3xl mx-auto text-center">
<div className="mb-8">
<Lock className="w-16 h-16 mx-auto mb-4 text-muted-foreground" />
<h1 className="text-2xl font-bold mb-2">Protected Page</h1>
<p className="text-muted-foreground mb-6">
You need to sign in to access the dashboard
</p>
</div>
<UserProfile />
</div>
</div>
);
}
return (
<div className="container mx-auto p-6">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 border border-border rounded-lg">
<h2 className="text-xl font-semibold mb-2">AI Chat</h2>
<p className="text-muted-foreground mb-4">
Start a conversation with AI using the Vercel AI SDK
</p>
{(diagnosticsLoading || !isAiReady) ? (
<Button disabled={true}>
Go to Chat
</Button>
) : (
<Button asChild>
<Link href="/chat">Go to Chat</Link>
</Button>
)}
</div>
<div className="p-6 border border-border rounded-lg">
<h2 className="text-xl font-semibold mb-2">Profile</h2>
<p className="text-muted-foreground mb-4">
Manage your account settings and preferences
</p>
<div className="space-y-2">
<p>
<strong>Name:</strong> {session.user.name}
</p>
<p>
<strong>Email:</strong> {session.user.email}
</p>
</div>
</div>
</div>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,117 @@
@import "tailwindcss";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,47 @@
import { ThemeProvider } from "@/components/theme-provider";
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { SiteHeader } from "@/components/site-header";
import { SiteFooter } from "@/components/site-footer";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Agentic Coding Boilerplate",
description:
"Complete agentic coding boilerplate with authentication, database, AI integration, and modern tooling - perfect for building AI-powered applications and autonomous agents by Leon van Zyl",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<SiteHeader />
{children}
<SiteFooter />
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,174 @@
"use client";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { SetupChecklist } from "@/components/setup-checklist";
import { useDiagnostics } from "@/hooks/use-diagnostics";
import { StarterPromptModal } from "@/components/starter-prompt-modal";
import { Video, Shield, Database, Palette, Bot } from "lucide-react";
export default function Home() {
const { isAuthReady, isAiReady, loading } = useDiagnostics();
return (
<main className="flex-1 container mx-auto px-4 py-12">
<div className="max-w-4xl mx-auto text-center space-y-8">
<div className="space-y-4">
<div className="flex items-center justify-center gap-3 mb-2">
<div className="flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10">
<Bot className="h-7 w-7 text-primary" />
</div>
<h1 className="text-5xl font-bold tracking-tight bg-gradient-to-r from-primary via-primary/90 to-primary/70 bg-clip-text text-transparent">
Starter Kit
</h1>
</div>
<h2 className="text-2xl font-semibold text-muted-foreground">
Complete Boilerplate for AI Applications
</h2>
<p className="text-xl text-muted-foreground">
A complete agentic coding boilerplate with authentication, database, AI
integration, and modern tooling for building AI-powered applications
</p>
</div>
{/* YouTube Tutorial Video */}
<div className="space-y-4">
<h3 className="text-2xl font-semibold flex items-center justify-center gap-2">
<Video className="h-6 w-6" />
Video Tutorial
</h3>
<p className="text-muted-foreground">
Watch the complete walkthrough of this agentic coding boilerplate:
</p>
<div className="relative w-full max-w-3xl mx-auto">
<div className="relative pb-[56.25%] h-0 overflow-hidden rounded-lg border">
<iframe
className="absolute top-0 left-0 w-full h-full"
src="https://www.youtube.com/embed/T0zFZsr_d0Q"
title="Agentic Coding Boilerplate Tutorial"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-12">
<div className="p-6 border rounded-lg">
<h3 className="font-semibold mb-2 flex items-center gap-2">
<Shield className="h-4 w-4" />
Authentication
</h3>
<p className="text-sm text-muted-foreground">
Better Auth with Google OAuth integration
</p>
</div>
<div className="p-6 border rounded-lg">
<h3 className="font-semibold mb-2 flex items-center gap-2">
<Database className="h-4 w-4" />
Database
</h3>
<p className="text-sm text-muted-foreground">
Drizzle ORM with PostgreSQL setup
</p>
</div>
<div className="p-6 border rounded-lg">
<h3 className="font-semibold mb-2 flex items-center gap-2">
<Bot className="h-4 w-4" />
AI Ready
</h3>
<p className="text-sm text-muted-foreground">
Vercel AI SDK with OpenAI integration
</p>
</div>
<div className="p-6 border rounded-lg">
<h3 className="font-semibold mb-2 flex items-center gap-2">
<Palette className="h-4 w-4" />
UI Components
</h3>
<p className="text-sm text-muted-foreground">
shadcn/ui with Tailwind CSS
</p>
</div>
</div>
<div className="space-y-6 mt-12">
<SetupChecklist />
<h3 className="text-2xl font-semibold">Next Steps</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
<div className="p-4 border rounded-lg">
<h4 className="font-medium mb-2">
1. Set up environment variables
</h4>
<p className="text-sm text-muted-foreground mb-2">
Copy <code>.env.example</code> to <code>.env.local</code> and
configure:
</p>
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
<li>POSTGRES_URL (PostgreSQL connection string)</li>
<li>GOOGLE_CLIENT_ID (OAuth credentials)</li>
<li>GOOGLE_CLIENT_SECRET (OAuth credentials)</li>
<li>OPENAI_API_KEY (for AI functionality)</li>
</ul>
</div>
<div className="p-4 border rounded-lg">
<h4 className="font-medium mb-2">2. Set up your database</h4>
<p className="text-sm text-muted-foreground mb-2">
Run database migrations:
</p>
<div className="space-y-2">
<code className="text-sm bg-muted p-2 rounded block">
npm run db:generate
</code>
<code className="text-sm bg-muted p-2 rounded block">
npm run db:migrate
</code>
</div>
</div>
<div className="p-4 border rounded-lg">
<h4 className="font-medium mb-2">3. Try the features</h4>
<div className="space-y-2">
{loading || !isAuthReady ? (
<Button size="sm" className="w-full" disabled={true}>
View Dashboard
</Button>
) : (
<Button asChild size="sm" className="w-full">
<Link href="/dashboard">View Dashboard</Link>
</Button>
)}
{loading || !isAiReady ? (
<Button
variant="outline"
size="sm"
className="w-full"
disabled={true}
>
Try AI Chat
</Button>
) : (
<Button
asChild
variant="outline"
size="sm"
className="w-full"
>
<Link href="/chat">Try AI Chat</Link>
</Button>
)}
</div>
</div>
<div className="p-4 border rounded-lg">
<h4 className="font-medium mb-2">4. Start building</h4>
<p className="text-sm text-muted-foreground mb-3">
Customize the components, add your own pages, and build your
application on top of this solid foundation.
</p>
<StarterPromptModal />
</div>
</div>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,222 @@
"use client";
import { useSession } from "@/lib/auth-client";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Mail, Calendar, User, Shield, ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";
export default function ProfilePage() {
const { data: session, isPending } = useSession();
const router = useRouter();
if (isPending) {
return (
<div className="flex items-center justify-center min-h-screen">
<div>Loading...</div>
</div>
);
}
if (!session) {
router.push("/");
return null;
}
const user = session.user;
const createdDate = user.createdAt ? new Date(user.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) : null;
return (
<div className="container max-w-4xl mx-auto py-8 px-4">
<div className="flex items-center gap-4 mb-8">
<Button
variant="ghost"
size="sm"
onClick={() => router.back()}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back
</Button>
<h1 className="text-3xl font-bold">Your Profile</h1>
</div>
<div className="grid gap-6">
{/* Profile Overview Card */}
<Card>
<CardHeader>
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage
src={user.image || ""}
alt={user.name || "User"}
referrerPolicy="no-referrer"
/>
<AvatarFallback className="text-lg">
{(
user.name?.[0] ||
user.email?.[0] ||
"U"
).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="space-y-2">
<h2 className="text-2xl font-semibold">{user.name}</h2>
<div className="flex items-center gap-2 text-muted-foreground">
<Mail className="h-4 w-4" />
<span>{user.email}</span>
{user.emailVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
<Shield className="h-3 w-3 mr-1" />
Verified
</Badge>
)}
</div>
{createdDate && (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Calendar className="h-4 w-4" />
<span>Member since {createdDate}</span>
</div>
)}
</div>
</div>
</CardHeader>
</Card>
{/* Account Information */}
<Card>
<CardHeader>
<CardTitle>Account Information</CardTitle>
<CardDescription>
Your account details and settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">
Full Name
</label>
<div className="p-3 border rounded-md bg-muted/10">
{user.name || "Not provided"}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">
Email Address
</label>
<div className="p-3 border rounded-md bg-muted/10 flex items-center justify-between">
<span>{user.email}</span>
{user.emailVerified && (
<Badge variant="outline" className="text-green-600 border-green-600">
Verified
</Badge>
)}
</div>
</div>
</div>
<Separator />
<div className="space-y-4">
<h3 className="text-lg font-medium">Account Status</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">Email Verification</p>
<p className="text-sm text-muted-foreground">
Email address verification status
</p>
</div>
<Badge variant={user.emailVerified ? "default" : "secondary"}>
{user.emailVerified ? "Verified" : "Unverified"}
</Badge>
</div>
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="space-y-1">
<p className="font-medium">Account Type</p>
<p className="text-sm text-muted-foreground">
Your account access level
</p>
</div>
<Badge variant="outline">Standard</Badge>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Account Activity */}
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Your recent account activity and sessions
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-3">
<div className="h-2 w-2 bg-green-500 rounded-full"></div>
<div>
<p className="font-medium">Current Session</p>
<p className="text-sm text-muted-foreground">Active now</p>
</div>
</div>
<Badge variant="outline" className="text-green-600 border-green-600">
Active
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Manage your account settings and preferences
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<User className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Edit Profile</div>
<div className="text-xs text-muted-foreground">Update your information</div>
</div>
</Button>
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Shield className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Security Settings</div>
<div className="text-xs text-muted-foreground">Manage security options</div>
</div>
</Button>
<Button variant="outline" className="justify-start h-auto p-4" disabled>
<Mail className="h-4 w-4 mr-2" />
<div className="text-left">
<div className="font-medium">Email Preferences</div>
<div className="text-xs text-muted-foreground">Configure notifications</div>
</div>
</Button>
</div>
<p className="text-xs text-muted-foreground mt-4">
Additional profile management features coming soon.
</p>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
"use client";
import { signIn, useSession } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
export function SignInButton() {
const { data: session, isPending } = useSession();
if (isPending) {
return <Button disabled>Loading...</Button>;
}
if (session) {
return null;
}
return (
<Button
onClick={async () => {
await signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
}}
>
Sign in
</Button>
);
}

View File

@@ -0,0 +1,31 @@
"use client";
import { signOut, useSession } from "@/lib/auth-client";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
export function SignOutButton() {
const { data: session, isPending } = useSession();
const router = useRouter();
if (isPending) {
return <Button disabled>Loading...</Button>;
}
if (!session) {
return null;
}
return (
<Button
variant="outline"
onClick={async () => {
await signOut();
router.replace("/");
router.refresh();
}}
>
Sign out
</Button>
);
}

View File

@@ -0,0 +1,84 @@
"use client";
import { useSession, signOut } from "@/lib/auth-client";
import { SignInButton } from "./sign-in-button";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { User, LogOut } from "lucide-react";
export function UserProfile() {
const { data: session, isPending } = useSession();
const router = useRouter();
if (isPending) {
return <div>Loading...</div>;
}
if (!session) {
return (
<div className="flex flex-col items-center gap-4 p-6">
<SignInButton />
</div>
);
}
const handleSignOut = async () => {
await signOut();
router.replace("/");
router.refresh();
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Avatar className="size-8 cursor-pointer hover:opacity-80 transition-opacity">
<AvatarImage
src={session.user?.image || ""}
alt={session.user?.name || "User"}
referrerPolicy="no-referrer"
/>
<AvatarFallback>
{(
session.user?.name?.[0] ||
session.user?.email?.[0] ||
"U"
).toUpperCase()}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{session.user?.name}
</p>
<p className="text-xs leading-none text-muted-foreground">
{session.user?.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/profile" className="flex items-center">
<User className="mr-2 h-4 w-4" />
Your Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleSignOut} variant="destructive">
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,148 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { CheckCircle2, XCircle } from "lucide-react";
type DiagnosticsResponse = {
timestamp: string;
env: {
POSTGRES_URL: boolean;
BETTER_AUTH_SECRET: boolean;
GOOGLE_CLIENT_ID: boolean;
GOOGLE_CLIENT_SECRET: boolean;
OPENAI_API_KEY: boolean;
NEXT_PUBLIC_APP_URL: boolean;
};
database: {
connected: boolean;
schemaApplied: boolean;
error?: string;
};
auth: {
configured: boolean;
routeResponding: boolean | null;
};
ai: {
configured: boolean;
};
overallStatus: "ok" | "warn" | "error";
};
function StatusIcon({ ok }: { ok: boolean }) {
return ok ? (
<div title="ok">
<CheckCircle2 className="h-4 w-4 text-green-600" aria-label="ok" />
</div>
) : (
<div title="not ok">
<XCircle className="h-4 w-4 text-red-600" aria-label="not-ok" />
</div>
);
}
export function SetupChecklist() {
const [data, setData] = useState<DiagnosticsResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/diagnostics", { cache: "no-store" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = (await res.json()) as DiagnosticsResponse;
setData(json);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to load diagnostics");
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
const steps = [
{
key: "env",
label: "Environment variables",
ok:
!!data?.env.POSTGRES_URL &&
!!data?.env.BETTER_AUTH_SECRET &&
!!data?.env.GOOGLE_CLIENT_ID &&
!!data?.env.GOOGLE_CLIENT_SECRET,
detail:
"Requires POSTGRES_URL, BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET",
},
{
key: "db",
label: "Database connected & schema",
ok: !!data?.database.connected && !!data?.database.schemaApplied,
detail: data?.database.error
? `Error: ${data.database.error}`
: undefined,
},
{
key: "auth",
label: "Auth configured",
ok: !!data?.auth.configured,
detail:
data?.auth.routeResponding === false
? "Auth route not responding"
: undefined,
},
{
key: "ai",
label: "AI integration (optional)",
ok: !!data?.ai.configured,
detail: !data?.ai.configured
? "Set OPENAI_API_KEY for AI chat"
: undefined,
},
] as const;
const completed = steps.filter((s) => s.ok).length;
return (
<div className="p-6 border rounded-lg text-left">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold">Setup checklist</h3>
<p className="text-sm text-muted-foreground">
{completed}/{steps.length} completed
</p>
</div>
<Button size="sm" onClick={load} disabled={loading}>
{loading ? "Checking..." : "Re-check"}
</Button>
</div>
{error ? <div className="text-sm text-destructive">{error}</div> : null}
<ul className="space-y-2">
{steps.map((s) => (
<li key={s.key} className="flex items-start gap-2">
<div className="mt-0.5">
<StatusIcon ok={Boolean(s.ok)} />
</div>
<div>
<div className="font-medium">{s.label}</div>
{s.detail ? (
<div className="text-sm text-muted-foreground">{s.detail}</div>
) : null}
</div>
</li>
))}
</ul>
{data ? (
<div className="mt-4 text-xs text-muted-foreground">
Last checked: {new Date(data.timestamp).toLocaleString()}
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,24 @@
import { GitHubStars } from "./ui/github-stars";
export function SiteFooter() {
return (
<footer className="border-t py-6 text-center text-sm text-muted-foreground">
<div className="container mx-auto px-4">
<div className="flex flex-col items-center space-y-3">
<GitHubStars repo="leonvanzyl/agentic-coding-starter-kit" />
<p>
Built using Agentic Coding Boilerplate by{" "}
<a
href="https://youtube.com/@leonvanzyl"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Leon van Zyl
</a>
</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,30 @@
import Link from "next/link";
import { UserProfile } from "@/components/auth/user-profile";
import { ModeToggle } from "./ui/mode-toggle";
import { Bot } from "lucide-react";
export function SiteHeader() {
return (
<header className="border-b">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">
<Link
href="/"
className="flex items-center gap-2 text-primary hover:text-primary/80 transition-colors"
>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
<Bot className="h-5 w-5" />
</div>
<span className="bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent">
Starter Kit
</span>
</Link>
</h1>
<div className="flex items-center gap-4">
<UserProfile />
<ModeToggle />
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,202 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Copy, Check } from "lucide-react";
const STARTER_PROMPT = `I'm working with an agentic coding boilerplate project that includes authentication, database integration, and AI capabilities. Here's what's already set up:
## Current Agentic Coding Boilerplate Structure
- **Authentication**: Better Auth with Google OAuth integration
- **Database**: Drizzle ORM with PostgreSQL setup
- **AI Integration**: Vercel AI SDK with OpenAI integration
- **UI**: shadcn/ui components with Tailwind CSS
- **Current Routes**:
- \`/\` - Home page with setup instructions and feature overview
- \`/dashboard\` - Protected dashboard page (requires authentication)
- \`/chat\` - AI chat interface (requires OpenAI API key)
## Important Context
This is an **agentic coding boilerplate/starter template** - all existing pages and components are meant to be examples and should be **completely replaced** to build the actual AI-powered application.
### CRITICAL: You MUST Override All Boilerplate Content
**DO NOT keep any boilerplate components, text, or UI elements unless explicitly requested.** This includes:
- **Remove all placeholder/demo content** (setup checklists, welcome messages, boilerplate text)
- **Replace the entire navigation structure** - don't keep the existing site header or nav items
- **Override all page content completely** - don't append to existing pages, replace them entirely
- **Remove or replace all example components** (setup-checklist, starter-prompt-modal, etc.)
- **Replace placeholder routes and pages** with the actual application functionality
### Required Actions:
1. **Start Fresh**: Treat existing components as temporary scaffolding to be removed
2. **Complete Replacement**: Build the new application from scratch using the existing tech stack
3. **No Hybrid Approach**: Don't try to integrate new features alongside existing boilerplate content
4. **Clean Slate**: The final application should have NO trace of the original boilerplate UI or content
The only things to preserve are:
- **All installed libraries and dependencies** (DO NOT uninstall or remove any packages from package.json)
- **Authentication system** (but customize the UI/flow as needed)
- **Database setup and schema** (but modify schema as needed for your use case)
- **Core configuration files** (next.config.ts, tsconfig.json, tailwind.config.ts, etc.)
- **Build and development scripts** (keep all npm/pnpm scripts in package.json)
## Tech Stack
- Next.js 15 with App Router
- TypeScript
- Tailwind CSS
- Better Auth for authentication
- Drizzle ORM + PostgreSQL
- Vercel AI SDK
- shadcn/ui components
- Lucide React icons
## AI Model Configuration
**IMPORTANT**: When implementing any AI functionality, always use the \`OPENAI_MODEL\` environment variable for the model name instead of hardcoding it:
\`\`\`typescript
// ✓ Correct - Use environment variable
const model = process.env.OPENAI_MODEL || "gpt-5-mini";
model: openai(model)
// ✗ Incorrect - Don't hardcode model names
model: openai("gpt-5-mini")
\`\`\`
This allows for easy model switching without code changes and ensures consistency across the application.
## Component Development Guidelines
**Always prioritize shadcn/ui components** when building the application:
1. **First Choice**: Use existing shadcn/ui components from the project
2. **Second Choice**: Install additional shadcn/ui components using \`pnpm dlx shadcn@latest add <component-name>\`
3. **Last Resort**: Only create custom components or use other libraries if shadcn/ui doesn't provide a suitable option
The project already includes several shadcn/ui components (button, dialog, avatar, etc.) and follows their design system. Always check the [shadcn/ui documentation](https://ui.shadcn.com/docs/components) for available components before implementing alternatives.
## What I Want to Build
[PROJECT_DESCRIPTION]
## Request
Please help me transform this boilerplate into my actual application. **You MUST completely replace all existing boilerplate code** to match my project requirements. The current implementation is just temporary scaffolding that should be entirely removed and replaced.
## Final Reminder: COMPLETE REPLACEMENT REQUIRED
**⚠️ IMPORTANT**: Do not preserve any of the existing boilerplate UI, components, or content. The user expects a completely fresh application that implements their requirements from scratch. Any remnants of the original boilerplate (like setup checklists, welcome screens, demo content, or placeholder navigation) indicate incomplete implementation.
**Success Criteria**: The final application should look and function as if it was built from scratch for the specific use case, with no evidence of the original boilerplate template.
## Post-Implementation Documentation
After completing the implementation, you MUST document any new features or significant changes in the \`/docs/features/\` directory:
1. **Create Feature Documentation**: For each major feature implemented, create a markdown file in \`/docs/features/\` that explains:
- What the feature does
- How it works
- Key components and files involved
- Usage examples
- Any configuration or setup required
2. **Update Existing Documentation**: If you modify existing functionality, update the relevant documentation files to reflect the changes.
3. **Document Design Decisions**: Include any important architectural or design decisions made during implementation.
This documentation helps maintain the project and assists future developers working with the codebase.
Think hard about the solution and implementing the user's requirements.`;
export function StarterPromptModal() {
const [isOpen, setIsOpen] = useState(false);
const [projectDescription, setProjectDescription] = useState("");
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
const finalPrompt = projectDescription.trim()
? STARTER_PROMPT.replace(
"[PROJECT_DESCRIPTION]",
projectDescription.trim()
)
: STARTER_PROMPT.replace("\n[PROJECT_DESCRIPTION]\n", "");
try {
await navigator.clipboard.writeText(finalPrompt);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button size="sm" className="w-full">
<Copy className="w-4 h-4 mr-2" />
Get AI Starter Prompt
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Generate AI Starter Prompt</DialogTitle>
<DialogDescription>
Create a comprehensive prompt to help AI agents create your project
for you.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label
htmlFor="project-description"
className="text-sm font-medium mb-2 block"
>
Describe your project (optional)
</label>
<textarea
id="project-description"
placeholder="e.g., A task management app for teams with real-time collaboration, project timelines, and AI-powered task prioritization..."
value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)}
className="w-full h-24 px-3 py-2 border rounded-md resize-none text-sm"
/>
<p className="text-xs text-muted-foreground mt-1">
Optional: Add details about your project to get a more tailored
prompt
</p>
</div>
<div className="flex gap-2">
<Button onClick={handleCopy} className="flex-1">
{copied ? (
<>
<Check className="w-4 h-4 mr-2" />
Copied!
</>
) : (
<>
<Copy className="w-4 h-4 mr-2" />
Copy Starter Prompt
</>
)}
</Button>
<Button variant="outline" onClick={() => setIsOpen(false)}>
Cancel
</Button>
</div>
<div className="text-xs text-muted-foreground border-t pt-3">
<strong>How to use:</strong> Copy this prompt and paste it into
Claude Code, Cursor, or any AI coding assistant to get started with
your project.
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,11 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -0,0 +1,53 @@
"use client";
import { useState, useEffect } from "react";
import { Github } from "lucide-react";
import { Button } from "@/components/ui/button";
interface GitHubStarsProps {
repo: string;
}
export function GitHubStars({ repo }: GitHubStarsProps) {
const [stars, setStars] = useState<number | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchStars() {
try {
const response = await fetch(`https://api.github.com/repos/${repo}`);
if (response.ok) {
const data = await response.json();
setStars(data.stargazers_count);
}
} catch (error) {
console.error("Failed to fetch GitHub stars:", error);
} finally {
setLoading(false);
}
}
fetchStars();
}, [repo]);
const formatStars = (count: number) => {
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k`;
}
return count.toString();
};
return (
<Button variant="outline" size="sm" asChild>
<a
href={`https://github.com/${repo}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<Github className="h-4 w-4" />
{loading ? "..." : stars !== null ? formatStars(stars) : "0"}
</a>
</Button>
);
}

View File

@@ -0,0 +1,40 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: "horizontal" | "vertical"
}
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
({ className, orientation = "horizontal", ...props }, ref) => (
<div
ref={ref}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = "Separator"
export { Separator }

View File

@@ -0,0 +1,68 @@
"use client";
import { useEffect, useState } from "react";
type DiagnosticsResponse = {
timestamp: string;
env: {
POSTGRES_URL: boolean;
BETTER_AUTH_SECRET: boolean;
GOOGLE_CLIENT_ID: boolean;
GOOGLE_CLIENT_SECRET: boolean;
OPENAI_API_KEY: boolean;
NEXT_PUBLIC_APP_URL: boolean;
};
database: {
connected: boolean;
schemaApplied: boolean;
error?: string;
};
auth: {
configured: boolean;
routeResponding: boolean | null;
};
ai: {
configured: boolean;
};
overallStatus: "ok" | "warn" | "error";
};
export function useDiagnostics() {
const [data, setData] = useState<DiagnosticsResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
async function fetchDiagnostics() {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/diagnostics", { cache: "no-store" });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = (await res.json()) as DiagnosticsResponse;
setData(json);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to load diagnostics");
} finally {
setLoading(false);
}
}
useEffect(() => {
fetchDiagnostics();
}, []);
const isAuthReady =
data?.auth.configured &&
data?.database.connected &&
data?.database.schemaApplied;
const isAiReady = data?.ai.configured;
return {
data,
loading,
error,
refetch: fetchDiagnostics,
isAuthReady: Boolean(isAuthReady),
isAiReady: Boolean(isAiReady),
};
}

View File

@@ -0,0 +1,13 @@
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
})
export const {
signIn,
signOut,
signUp,
useSession,
getSession,
} = authClient

View File

@@ -0,0 +1,15 @@
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "./db"
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
})

View File

@@ -0,0 +1,12 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
const connectionString = process.env.POSTGRES_URL as string;
if (!connectionString) {
throw new Error("POSTGRES_URL environment variable is not set");
}
const client = postgres(connectionString);
export const db = drizzle(client, { schema });

View File

@@ -0,0 +1,51 @@
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("emailVerified"),
image: text("image"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expiresAt").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
ipAddress: text("ipAddress"),
userAgent: text("userAgent"),
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("accountId").notNull(),
providerId: text("providerId").notNull(),
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("accessToken"),
refreshToken: text("refreshToken"),
idToken: text("idToken"),
accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expiresAt").notNull(),
createdAt: timestamp("createdAt").defaultNow(),
updatedAt: timestamp("updatedAt").defaultNow(),
});

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"types": ["react", "react-dom", "node"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}