- Replace <select>, <input>, <label>, <textarea> with UI components - Update various dashboard and docs pages - Simplify unsandbox package.json - Add DESIGN_SYSTEM.md documentation - Add Claude skills configuration
1170 lines
No EOL
31 KiB
Markdown
1170 lines
No EOL
31 KiB
Markdown
|
|
## Monorepo Setup
|
|
|
|
This project uses a Turborepo monorepo architecture with the following structure:
|
|
|
|
### Packages
|
|
|
|
**Published to npm (@tpmjs scope):**
|
|
- `@tpmjs/ui` - React component library with .ts-only components
|
|
- `@tpmjs/utils` - Utility functions (cn, format, etc.)
|
|
- `@tpmjs/types` - Shared TypeScript types and Zod schemas
|
|
- `@tpmjs/env` - Environment variable validation with Zod
|
|
|
|
**Internal tooling (private):**
|
|
- `@tpmjs/config` - Shared configurations (Biome, ESLint, Tailwind, TypeScript)
|
|
- `@tpmjs/eslint-config` - ESLint configuration with module boundary rules
|
|
- `@tpmjs/tailwind-config` - Tailwind configuration with design tokens
|
|
- `@tpmjs/tsconfig` - TypeScript configurations (base, nextjs, react-library)
|
|
- `@tpmjs/test` - Vitest shared configuration
|
|
- `@tpmjs/mocks` - MSW mock server for testing
|
|
- `@tpmjs/storybook` - Component documentation and showcase
|
|
|
|
### Applications
|
|
|
|
- `@tpmjs/web` - Next.js 16 App Router application (main website)
|
|
|
|
### Architecture Principles
|
|
|
|
#### 1. Design System First
|
|
|
|
**Always use `@tpmjs/ui` components instead of raw HTML elements.** This ensures visual consistency, accessibility, and maintainability across the application.
|
|
|
|
```typescript
|
|
// Good - use design system components
|
|
import { Button } from '@tpmjs/ui/Button/Button';
|
|
import { Input } from '@tpmjs/ui/Input/Input';
|
|
import { Table, TableRow, TableCell } from '@tpmjs/ui/Table/Table';
|
|
|
|
<Button onClick={handleClick}>Submit</Button>
|
|
<Input value={value} onChange={onChange} />
|
|
|
|
// Bad - raw HTML elements
|
|
<button onClick={handleClick}>Submit</button>
|
|
<input value={value} onChange={onChange} />
|
|
```
|
|
|
|
**When to create/update UI components:**
|
|
- If a pattern is used in 2+ places, create a reusable component in `@tpmjs/ui`
|
|
- If existing component styling doesn't match the design, update the component (not the usage site)
|
|
- If you need virtualization (e.g., `react-virtuoso`), match the design system's styling classes
|
|
|
|
**Common components to use:**
|
|
- `Button` - all clickable actions
|
|
- `Input`, `Select`, `Textarea` - form inputs
|
|
- `Table`, `TableRow`, `TableCell` - data tables
|
|
- `Card` - content containers
|
|
- `Badge` - status indicators
|
|
- `Icon` - all icons (not inline SVGs)
|
|
- `Spinner`, `Skeleton` - loading states
|
|
|
|
#### 2. No Barrel Exports
|
|
|
|
Components are imported directly without `index.ts` files:
|
|
|
|
```typescript
|
|
// Good
|
|
import { Button } from '@tpmjs/ui/Button/Button';
|
|
|
|
// Bad (not allowed)
|
|
import { Button } from '@tpmjs/ui';
|
|
```
|
|
|
|
**Benefits:**
|
|
- Clearer dependency graphs
|
|
- Better tree-shaking
|
|
- Prevents circular dependencies
|
|
- Explicit imports
|
|
|
|
#### 3. Module Boundaries
|
|
|
|
ESLint enforces strict module boundaries:
|
|
- Apps can only import from published packages
|
|
- Packages cannot import from apps
|
|
- UI package cannot import from utils (stays dependency-free)
|
|
|
|
#### 4. Shared Configurations
|
|
|
|
All configuration is centralized in `packages/config/`:
|
|
- **Biome** - Formatting + basic linting
|
|
- **ESLint** - Semantic rules and module boundaries
|
|
- **Tailwind** - Design tokens and shared theme
|
|
- **TypeScript** - Multiple configs for different contexts
|
|
|
|
### Development Workflow
|
|
|
|
```bash
|
|
# Install dependencies
|
|
pnpm install
|
|
|
|
# Run development servers
|
|
pnpm dev
|
|
|
|
# Build all packages
|
|
pnpm build
|
|
|
|
# Run tests
|
|
pnpm test
|
|
|
|
# Lint and format
|
|
pnpm lint
|
|
pnpm format
|
|
```
|
|
|
|
### Detailed Development Commands
|
|
|
|
This section documents the complete testing, building, and development workflow used in this monorepo.
|
|
|
|
#### Testing Individual Packages
|
|
|
|
Use the `--filter` flag to target specific packages:
|
|
|
|
```bash
|
|
# Type-check a single package
|
|
pnpm --filter=@tpmjs/npm-client type-check
|
|
pnpm --filter=@tpmjs/ui type-check
|
|
pnpm --filter=@tpmjs/web type-check
|
|
|
|
# Run tests in a single package
|
|
pnpm --filter=@tpmjs/ui test
|
|
pnpm --filter=@tpmjs/web test
|
|
|
|
# Lint a single package
|
|
pnpm --filter=@tpmjs/web lint
|
|
```
|
|
|
|
#### Testing All Packages
|
|
|
|
Commands from the root run across all packages via Turborepo:
|
|
|
|
```bash
|
|
# Type-check all packages (runs via Turborepo)
|
|
pnpm type-check
|
|
|
|
# Lint all packages (runs via Turborepo)
|
|
pnpm lint
|
|
|
|
# Format all files with Biome
|
|
pnpm format
|
|
|
|
# Test all packages (runs via Turborepo)
|
|
pnpm test
|
|
```
|
|
|
|
#### Building Packages
|
|
|
|
Build commands respect dependency order automatically:
|
|
|
|
```bash
|
|
# Build a single package (and its dependencies)
|
|
pnpm --filter=@tpmjs/ui build
|
|
pnpm --filter=@tpmjs/types build
|
|
|
|
# Build all packages
|
|
pnpm build
|
|
|
|
# Build and watch for changes
|
|
pnpm --filter=@tpmjs/ui dev
|
|
```
|
|
|
|
#### Database Commands (Prisma)
|
|
|
|
The `@tpmjs/db` package uses Prisma for database management:
|
|
|
|
```bash
|
|
# Generate Prisma client (required after schema changes)
|
|
pnpm --filter=@tpmjs/db db:generate
|
|
|
|
# Push schema changes to database (dev)
|
|
pnpm --filter=@tpmjs/db db:push
|
|
|
|
# Create and apply migrations (production)
|
|
pnpm --filter=@tpmjs/db db:migrate
|
|
|
|
# Open Prisma Studio (database GUI)
|
|
pnpm --filter=@tpmjs/db db:studio
|
|
|
|
# Seed the database
|
|
pnpm --filter=@tpmjs/db db:seed
|
|
```
|
|
|
|
**Important:** Always run `pnpm --filter=@tpmjs/db db:generate` after modifying `schema.prisma` to regenerate the Prisma client. Without this, TypeScript will show errors for database types.
|
|
|
|
#### Development Servers
|
|
|
|
```bash
|
|
# Run Next.js dev server for web app
|
|
pnpm dev --filter=@tpmjs/web
|
|
|
|
# Run all dev servers (if multiple apps)
|
|
pnpm dev
|
|
|
|
# Run Storybook for component development
|
|
pnpm --filter=@tpmjs/storybook dev
|
|
```
|
|
|
|
#### Pre-commit Hooks (Lefthook)
|
|
|
|
Git commits automatically trigger these checks via Lefthook:
|
|
|
|
1. **Format** - Biome formats all staged files
|
|
2. **Lint** - Runs `pnpm lint` across all packages
|
|
3. **Type-check** - Runs `pnpm type-check` across all packages
|
|
|
|
If any check fails, the commit is blocked. The hooks ensure code quality before changes reach CI.
|
|
|
|
**Note:** Pre-commit hooks run the same checks as CI, so if they pass locally, CI should pass too.
|
|
|
|
#### Turborepo Caching
|
|
|
|
Turborepo caches task outputs for faster rebuilds:
|
|
|
|
- **Cache hits**: Tasks show `cache hit, replaying logs` - no actual work done
|
|
- **Cache miss**: Tasks execute normally and outputs are cached
|
|
- **Invalidation**: Cache invalidates when inputs change (source files, dependencies, env vars)
|
|
|
|
```bash
|
|
# Clear Turborepo cache if needed
|
|
pnpm turbo clean
|
|
|
|
# Force rebuild without cache
|
|
pnpm build --force
|
|
```
|
|
|
|
#### Common Workflows
|
|
|
|
**After pulling new changes:**
|
|
```bash
|
|
pnpm install # Install new dependencies
|
|
pnpm db:generate # Regenerate Prisma client if schema changed
|
|
pnpm type-check # Verify everything type-checks
|
|
pnpm dev --filter=@tpmjs/web # Start dev server
|
|
```
|
|
|
|
**Creating a new package:**
|
|
```bash
|
|
# 1. Create package directory and files
|
|
mkdir -p packages/my-package/src
|
|
cd packages/my-package
|
|
|
|
# 2. Create package.json with proper name and workspace dependencies
|
|
# 3. Create tsconfig.json extending @tpmjs/tsconfig
|
|
|
|
# 4. Install dependencies from root
|
|
cd ../..
|
|
pnpm install
|
|
|
|
# 5. Type-check the new package
|
|
pnpm --filter=@tpmjs/my-package type-check
|
|
```
|
|
|
|
**Testing before committing:**
|
|
```bash
|
|
# Run the same checks that pre-commit hooks will run
|
|
pnpm format # Format all files
|
|
pnpm lint # Lint all packages
|
|
pnpm type-check # Type-check all packages
|
|
|
|
# Then commit - hooks should pass quickly
|
|
git add .
|
|
git commit -m "your message"
|
|
```
|
|
|
|
#### Troubleshooting
|
|
|
|
**"Cannot find module '@prisma/client'"**
|
|
- Run `pnpm --filter=@tpmjs/db db:generate` to generate the Prisma client
|
|
- The Prisma client must be generated after any schema changes or fresh installs
|
|
|
|
**"Type error in package that imports from another package"**
|
|
- Build the dependency first: `pnpm --filter=@tpmjs/types build`
|
|
- Or build all packages: `pnpm build`
|
|
- Turborepo handles this automatically when using `pnpm build`
|
|
|
|
**"Biome formatting errors in pre-commit"**
|
|
- Run `pnpm format` to auto-fix formatting issues
|
|
- Biome will format all files according to the config
|
|
|
|
**"ESLint warnings about module boundaries"**
|
|
- Check that you're not importing from apps in packages
|
|
- Check that imports follow the no-barrel-exports rule
|
|
- Example: Use `@tpmjs/ui/Button/Button` not `@tpmjs/ui`
|
|
|
|
**"Turborepo cache shows stale outputs"**
|
|
- Clear cache with `pnpm turbo clean`
|
|
- Force rebuild with `pnpm build --force`
|
|
|
|
**"Dev server won't start"**
|
|
- Check that all dependencies are installed: `pnpm install`
|
|
- Check that Prisma client is generated: `pnpm db:generate`
|
|
- Check for port conflicts (Next.js default: 3000)
|
|
|
|
### Publishing Flow
|
|
|
|
1. Make changes to packages
|
|
2. Create changeset: `pnpm changeset`
|
|
3. Version packages: `pnpm changeset:version`
|
|
4. Publish to npm: `pnpm changeset:publish`
|
|
5. Push with tags: `git push --follow-tags`
|
|
|
|
### Tech Stack
|
|
|
|
- **Build System:** Turborepo
|
|
- **Package Manager:** pnpm
|
|
- **TypeScript:** Strict mode, composite projects
|
|
- **React:** v19
|
|
- **Next.js:** v16 App Router
|
|
- **Styling:** Tailwind CSS
|
|
- **Testing:** Vitest + Testing Library
|
|
- **Linting:** Biome + ESLint
|
|
- **Documentation:** Storybook
|
|
- **CI/CD:** GitHub Actions + Changesets
|
|
- **Git Hooks:** Lefthook
|
|
|
|
### Debugging CI/CD with CLI Tools
|
|
|
|
When debugging CI failures or deployment issues, use command-line tools for efficient investigation:
|
|
|
|
#### GitHub CLI (`gh`)
|
|
|
|
Debug GitHub Actions CI runs:
|
|
|
|
```bash
|
|
# List recent workflow runs
|
|
gh run list --limit 10
|
|
|
|
# View specific run details
|
|
gh run view <run-id>
|
|
|
|
# View failed job logs
|
|
gh run view <run-id> --log-failed
|
|
|
|
# View specific job logs
|
|
gh run view <run-id> --job <job-id> --log
|
|
|
|
# Rerun failed jobs
|
|
gh run rerun <run-id> --failed
|
|
```
|
|
|
|
**Common debugging workflow:**
|
|
1. `gh run list` - Find the failed run ID
|
|
2. `gh run view <run-id> --log-failed` - See what failed
|
|
3. Fix the issue locally
|
|
4. Push and monitor: `gh run watch`
|
|
|
|
#### Vercel CLI
|
|
|
|
Debug deployments and preview environments:
|
|
|
|
```bash
|
|
# List deployments
|
|
vercel ls
|
|
|
|
# View deployment details
|
|
vercel inspect <deployment-url>
|
|
|
|
# View deployment logs
|
|
vercel logs <deployment-url>
|
|
|
|
# Pull environment variables
|
|
vercel env pull
|
|
|
|
# Link local project to Vercel project
|
|
vercel link
|
|
```
|
|
|
|
**Common debugging workflow:**
|
|
1. `vercel ls` - Find the deployment URL
|
|
2. `vercel inspect <url>` - Check deployment status and build logs
|
|
3. `vercel logs <url>` - View runtime logs
|
|
4. Compare env vars: `vercel env pull` and check `.env.local`
|
|
|
|
#### Tips
|
|
|
|
- Use `gh` and `vercel` CLIs to debug without leaving the terminal
|
|
- Check CI logs before making blind fixes
|
|
- Vercel deployments are blocked until GitHub Actions pass (configured in vercel.json)
|
|
- Pre-commit/pre-push hooks run the same checks as CI - if they pass locally, CI should pass too
|
|
|
|
---
|
|
|
|
## Case Study: Fixing API Route Timeouts on tpmjs.com
|
|
|
|
This is a detailed account of debugging and fixing API route timeouts in production. The investigation revealed critical insights about deploying Turborepo monorepos to Vercel with Prisma.
|
|
|
|
### The Problem
|
|
|
|
After deploying tpmjs.com to production, all API endpoints were timing out:
|
|
|
|
```bash
|
|
$ curl https://tpmjs.com/api/health
|
|
# Request timed out after 60 seconds
|
|
|
|
$ curl https://tpmjs.com/api/tools
|
|
# Request timed out after 60 seconds
|
|
```
|
|
|
|
The Next.js UI worked perfectly - pages loaded, navigation functioned - but every API route request resulted in a timeout. No errors appeared in Vercel logs, and the requests never even reached the serverless functions.
|
|
|
|
### Initial Investigation
|
|
|
|
**Step 1: Verify Build Output**
|
|
|
|
```bash
|
|
vercel inspect <deployment-url>
|
|
```
|
|
|
|
The build showed pages but **no API routes** listed as lambda functions:
|
|
|
|
```
|
|
Builds
|
|
├── ○ / (static page)
|
|
├── ○ /playground (static page)
|
|
└── ○ /tool/[slug] (static page)
|
|
|
|
# Expected to see:
|
|
├── λ api/health
|
|
├── λ api/tools
|
|
└── λ api/sync/changes
|
|
```
|
|
|
|
This confirmed Vercel wasn't treating the project as Next.js - it was using static site generation and dropping all API routes.
|
|
|
|
**Step 2: Check Vercel Configuration**
|
|
|
|
Examined `apps/web/vercel.json`:
|
|
|
|
```json
|
|
{
|
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web",
|
|
"installCommand": "pnpm install"
|
|
}
|
|
```
|
|
|
|
The build command looked correct, but the custom build command was bypassing Vercel's Next.js detection.
|
|
|
|
### Root Cause #1: Workspace Dependencies Not Built
|
|
|
|
Deployed the site and checked the build logs. Found this critical error:
|
|
|
|
```
|
|
Module not found: Can't resolve '@tpmjs/env'
|
|
Module not found: Can't resolve '@tpmjs/types/tpmjs'
|
|
Module not found: Can't resolve '@tpmjs/ui/Badge/Badge'
|
|
Package @prisma/client can't be external
|
|
```
|
|
|
|
**49 module resolution errors** - the workspace packages weren't being built before the web app tried to import them.
|
|
|
|
**The Fix:**
|
|
|
|
Changed the build command from:
|
|
```json
|
|
"buildCommand": "cd ../.. && turbo build --filter=@tpmjs/web"
|
|
```
|
|
|
|
To:
|
|
```json
|
|
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build"
|
|
```
|
|
|
|
The `...` suffix in `--filter=@tpmjs/web...` tells pnpm to build ALL dependencies first:
|
|
|
|
1. Build `@tpmjs/env`
|
|
2. Build `@tpmjs/types`
|
|
3. Build `@tpmjs/ui`
|
|
4. Build `@tpmjs/utils`
|
|
5. Build `@tpmjs/db` (Prisma generate)
|
|
6. Finally build `@tpmjs/web`
|
|
|
|
After this change, the build succeeded and API routes appeared as lambda functions in `vercel inspect`.
|
|
|
|
### Root Cause #2: Prisma Cold Start Performance
|
|
|
|
With the build fixed, API routes were deployed but still timing out. Testing revealed:
|
|
|
|
```bash
|
|
# Health endpoint (no database) - WORKS
|
|
$ curl https://tpmjs.com/api/health
|
|
{"status":"ok","timestamp":"2025-11-28T11:32:29.295Z"}
|
|
|
|
# Tools endpoint (with database) - TIMEOUT
|
|
$ curl https://tpmjs.com/api/tools
|
|
# ...60 second timeout
|
|
```
|
|
|
|
**Local Database Performance Test:**
|
|
|
|
```javascript
|
|
// Test the exact queries used in production
|
|
const count = await prisma.tool.count(); // 3.244s ⚠️
|
|
const tools = await prisma.tool.findMany({
|
|
orderBy: [
|
|
{ qualityScore: 'desc' },
|
|
{ npmDownloadsLastMonth: 'desc' },
|
|
{ createdAt: 'desc' },
|
|
],
|
|
take: 20,
|
|
}); // 593ms ⚠️
|
|
```
|
|
|
|
The parallel `count()` + `findMany()` queries were taking **3.8 seconds** due to Prisma cold start in serverless environments.
|
|
|
|
**The Fix:**
|
|
|
|
Optimized the endpoint in two ways:
|
|
|
|
1. **Removed expensive count query** - Using `count()` in every request is slow and usually unnecessary:
|
|
|
|
```typescript
|
|
// Before: Slow parallel queries
|
|
const [tools, totalCount] = await Promise.all([
|
|
prisma.tool.findMany({ where, take: limit, skip: offset }),
|
|
prisma.tool.count({ where }), // ← 3+ seconds!
|
|
]);
|
|
|
|
// After: Fast single query with limit+1 technique
|
|
const tools = await prisma.tool.findMany({
|
|
where,
|
|
take: limit + 1, // Fetch one extra to check if more exist
|
|
skip: offset,
|
|
});
|
|
|
|
const hasMore = tools.length > limit;
|
|
const actualTools = hasMore ? tools.slice(0, limit) : tools;
|
|
```
|
|
|
|
2. **Reduced max page size** from 100 to 50 items for better performance
|
|
|
|
**Results:**
|
|
|
|
```bash
|
|
$ curl https://tpmjs.com/api/tools
|
|
{
|
|
"success": true,
|
|
"data": [...], # Returns in <1 second
|
|
"pagination": {
|
|
"limit": 20,
|
|
"offset": 0,
|
|
"hasMore": false
|
|
}
|
|
}
|
|
```
|
|
|
|
### Additional Optimizations Applied
|
|
|
|
**Added maxDuration to all API routes:**
|
|
|
|
```typescript
|
|
export const runtime = 'nodejs';
|
|
export const dynamic = 'force-dynamic';
|
|
export const maxDuration = 60; // ← Prevent premature timeouts
|
|
```
|
|
|
|
**Verified database indexes exist:**
|
|
|
|
```prisma
|
|
model Tool {
|
|
// ... fields ...
|
|
|
|
@@index([category])
|
|
@@index([isOfficial])
|
|
@@index([qualityScore])
|
|
@@index([npmDownloadsLastMonth])
|
|
@@index([createdAt])
|
|
}
|
|
```
|
|
|
|
All necessary indexes were present - the issue was cold start latency, not missing indexes.
|
|
|
|
### Key Lessons Learned
|
|
|
|
**1. Monorepo Build Order Matters**
|
|
|
|
When deploying Turborepo monorepos to Vercel, workspace dependencies MUST be built first:
|
|
|
|
```bash
|
|
# ❌ Wrong - only builds the web app
|
|
pnpm --filter=@tpmjs/web build
|
|
|
|
# ✅ Correct - builds dependencies first
|
|
pnpm --filter=@tpmjs/web... build
|
|
```
|
|
|
|
**2. Prisma in Serverless = Slow First Request**
|
|
|
|
Prisma Client initialization in serverless environments adds 1-3 seconds of latency on cold starts. Strategies to mitigate:
|
|
|
|
- Use connection pooling (Neon, PlanetScale)
|
|
- Eliminate unnecessary queries (especially `count()`)
|
|
- Cache query results when possible
|
|
- Consider Prisma Accelerate for critical paths
|
|
|
|
**3. Progressive Debugging Approach**
|
|
|
|
Start simple and progressively add complexity:
|
|
|
|
```typescript
|
|
// Step 1: Does endpoint respond at all?
|
|
export async function GET() {
|
|
return NextResponse.json({ status: 'ok' });
|
|
}
|
|
|
|
// Step 2: Can we connect to database?
|
|
export async function GET() {
|
|
return NextResponse.json({
|
|
hasDatabase: !!process.env.DATABASE_URL,
|
|
});
|
|
}
|
|
|
|
// Step 3: Can we query the database?
|
|
export async function GET() {
|
|
const count = await prisma.tool.count();
|
|
return NextResponse.json({ count });
|
|
}
|
|
|
|
// Step 4: Full implementation with optimizations
|
|
```
|
|
|
|
**4. Use Vercel CLI for Debugging**
|
|
|
|
```bash
|
|
# Check what's actually deployed
|
|
vercel inspect <deployment-url>
|
|
|
|
# Look for lambda functions (λ)
|
|
Builds
|
|
├── λ api/health ✅
|
|
├── λ api/tools ✅
|
|
|
|
# If you see only static pages (○), API routes aren't deployed
|
|
```
|
|
|
|
### Final Configuration
|
|
|
|
**`apps/web/vercel.json`:**
|
|
```json
|
|
{
|
|
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
"buildCommand": "cd ../.. && pnpm install && pnpm --filter=@tpmjs/web... build",
|
|
"installCommand": "pnpm install"
|
|
}
|
|
```
|
|
|
|
**API Route Template:**
|
|
```typescript
|
|
import { prisma } from '@tpmjs/db';
|
|
import { NextResponse } from 'next/server';
|
|
|
|
export const runtime = 'nodejs';
|
|
export const dynamic = 'force-dynamic';
|
|
export const maxDuration = 60;
|
|
|
|
export async function GET() {
|
|
try {
|
|
// Avoid count() - use limit+1 technique instead
|
|
const items = await prisma.tool.findMany({
|
|
orderBy: { qualityScore: 'desc' },
|
|
take: 21, // Request 1 more than needed
|
|
});
|
|
|
|
const hasMore = items.length > 20;
|
|
const data = hasMore ? items.slice(0, 20) : items;
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data,
|
|
pagination: { hasMore },
|
|
});
|
|
} catch (error) {
|
|
return NextResponse.json(
|
|
{ success: false, error: error.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Performance Metrics
|
|
|
|
**Before Optimization:**
|
|
- `/api/health`: Timeout (60s+)
|
|
- `/api/tools`: Timeout (60s+)
|
|
- Build: Failed (module resolution errors)
|
|
|
|
**After Optimization:**
|
|
- `/api/health`: ~50ms ✅
|
|
- `/api/tools`: ~800ms ✅
|
|
- Build: Success (all deps built) ✅
|
|
|
|
### Conclusion
|
|
|
|
API timeouts in serverless environments often stem from build configuration issues or database cold starts. For Turborepo + Vercel + Prisma:
|
|
|
|
1. Use `pnpm --filter=package...` to build dependencies
|
|
2. Avoid `count()` queries in hot paths
|
|
3. Add `maxDuration` to API routes
|
|
4. Use `vercel inspect` to verify lambda deployment
|
|
5. Test database performance locally before deploying
|
|
|
|
The full working implementation is live at [tpmjs.com](https://tpmjs.com).
|
|
|
|
---
|
|
|
|
## NPM Package Syncing System
|
|
|
|
TPMJS.com automatically mirrors npm packages with the `tpmjs` keyword to keep the tool registry up-to-date. This section documents how the syncing system works.
|
|
|
|
### Overview
|
|
|
|
The sync system uses three automated strategies running on Vercel Cron to discover and update TPMJS tools:
|
|
|
|
1. **Changes Feed** - Monitors npm's real-time changes feed for all package updates
|
|
2. **Keyword Search** - Actively searches npm for packages with the `tpmjs` keyword
|
|
3. **Metrics Sync** - Updates download stats and calculates quality scores
|
|
|
|
### Sync Endpoints
|
|
|
|
All sync endpoints are located in `apps/web/src/app/api/sync/`:
|
|
|
|
#### 1. Changes Feed Sync (`/api/sync/changes`)
|
|
|
|
**Purpose:** Monitors npm's changes feed to catch new packages and updates in real-time.
|
|
|
|
**Schedule:** Every 2 minutes (`*/2 * * * *`)
|
|
|
|
**How it works:**
|
|
1. Fetches the last checkpoint sequence number from the database
|
|
2. Calls npm's `/_changes` endpoint with `since=<lastSeq>` (limit 100 per run)
|
|
3. For each changed package, fetches full metadata with `fetchLatestPackageWithMetadata()`
|
|
4. Validates that the package has a valid `tpmjs` field using `validateTpmjsField()`
|
|
5. Upserts the tool to the database with `discoveryMethod: 'changes-feed'`
|
|
6. Updates the checkpoint with the new sequence number for next run
|
|
|
|
**Key Features:**
|
|
- Uses checkpoints to track progress and avoid reprocessing
|
|
- Processes up to 100 changes per run to avoid timeouts
|
|
- Logs all sync operations to `syncLog` table
|
|
- Requires `Authorization: Bearer <CRON_SECRET>` header
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"processed": 5,
|
|
"skipped": 93,
|
|
"errors": 0,
|
|
"lastSeq": "12345678",
|
|
"pending": 1250,
|
|
"durationMs": 2834
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. Keyword Search Sync (`/api/sync/keyword`)
|
|
|
|
**Purpose:** Actively searches npm for packages with the `tpmjs` keyword.
|
|
|
|
**Schedule:** Every 15 minutes (`*/15 * * * *`)
|
|
|
|
**How it works:**
|
|
1. Searches npm registry for packages with keyword `tpmjs` (up to 250 results)
|
|
2. Fetches full metadata for each package
|
|
3. Validates the `tpmjs` field
|
|
4. Upserts tools with `discoveryMethod: 'keyword'`
|
|
5. Updates checkpoint with last run timestamp
|
|
|
|
**Key Features:**
|
|
- Catches packages that might be missed by changes feed
|
|
- Useful for backfilling existing packages
|
|
- Processes up to 250 packages per run
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"processed": 12,
|
|
"skipped": 3,
|
|
"errors": 0,
|
|
"packagesFound": 15,
|
|
"durationMs": 4521
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. Metrics Sync (`/api/sync/metrics`)
|
|
|
|
**Purpose:** Updates download statistics and calculates quality scores for all tools.
|
|
|
|
**Schedule:** Every hour (`0 * * * *`)
|
|
|
|
**How it works:**
|
|
1. Fetches all tools from the database
|
|
2. For each tool, calls `fetchDownloadStats()` to get last 30 days of downloads
|
|
3. Calculates quality score based on:
|
|
- Tier (rich = 0.6, minimal = 0.4)
|
|
- Downloads (logarithmic scale, max 0.3)
|
|
- GitHub stars (logarithmic scale, max 0.1)
|
|
4. Updates `npmDownloadsLastMonth` and `qualityScore` fields
|
|
|
|
**Quality Score Formula:**
|
|
```typescript
|
|
function calculateQualityScore(params: {
|
|
tier: string;
|
|
downloads: number;
|
|
githubStars: number;
|
|
}): number {
|
|
const tierScore = tier === 'rich' ? 0.6 : 0.4;
|
|
const downloadsScore = Math.min(0.3, Math.log10(downloads + 1) / 10);
|
|
const starsScore = Math.min(0.1, Math.log10(githubStars + 1) / 10);
|
|
return Math.min(1.0, tierScore + downloadsScore + starsScore);
|
|
}
|
|
```
|
|
|
|
**Example Response:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"processed": 25,
|
|
"skipped": 0,
|
|
"errors": 0,
|
|
"totalTools": 25,
|
|
"durationMs": 8234
|
|
}
|
|
}
|
|
```
|
|
|
|
### Automated Sync Configuration
|
|
|
|
The sync system can run via two methods:
|
|
|
|
#### Option 1: Vercel Cron (Primary)
|
|
|
|
Cron jobs are configured in `vercel.json` at the repository root:
|
|
|
|
```json
|
|
{
|
|
"crons": [
|
|
{
|
|
"path": "/api/sync/changes",
|
|
"schedule": "*/2 * * * *"
|
|
},
|
|
{
|
|
"path": "/api/sync/keyword",
|
|
"schedule": "*/15 * * * *"
|
|
},
|
|
{
|
|
"path": "/api/sync/metrics",
|
|
"schedule": "0 * * * *"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Pros:**
|
|
- Native Vercel integration
|
|
- Automatic authentication with `CRON_SECRET`
|
|
- Same infrastructure as the app
|
|
- No setup required (works automatically on deploy)
|
|
|
|
#### Option 2: GitHub Actions (Backup)
|
|
|
|
A GitHub Actions workflow (`.github/workflows/sync.yml`) provides redundancy:
|
|
|
|
```yaml
|
|
name: NPM Package Sync
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '*/2 * * * *' # Changes feed
|
|
- cron: '*/15 * * * *' # Keyword search
|
|
- cron: '0 * * * *' # Metrics
|
|
workflow_dispatch: # Manual trigger
|
|
```
|
|
|
|
**Pros:**
|
|
- Redundancy if Vercel Cron fails
|
|
- Manual trigger via GitHub UI
|
|
- Free on GitHub (included in free tier)
|
|
- Runs from GitHub's infrastructure
|
|
|
|
**Setup:**
|
|
|
|
1. Add secrets to GitHub repository settings:
|
|
- `VERCEL_PRODUCTION_URL` - Your production URL (e.g., `https://tpmjs.com`)
|
|
- `CRON_SECRET` - Same secret used in Vercel environment variables
|
|
|
|
2. Enable GitHub Actions in repository settings
|
|
|
|
3. The workflow will run automatically on schedule OR manually via:
|
|
- GitHub Actions tab → NPM Package Sync → Run workflow → Select sync type
|
|
|
|
**Schedule Breakdown:**
|
|
- Changes feed: Every 2 minutes (30 times per hour)
|
|
- Keyword search: Every 15 minutes (4 times per hour)
|
|
- Metrics: Every hour (once per hour)
|
|
|
|
**Recommendation:** Use Vercel Cron as primary and GitHub Actions as backup. Both can run simultaneously - the sync endpoints are idempotent.
|
|
|
|
### Database Schema
|
|
|
|
The sync system uses these Prisma models:
|
|
|
|
**`Tool` - The main tool registry:**
|
|
```prisma
|
|
model Tool {
|
|
id String @id @default(cuid())
|
|
npmPackageName String @unique
|
|
npmVersion String
|
|
npmDownloadsLastMonth Int @default(0)
|
|
qualityScore Float?
|
|
discoveryMethod String // 'changes-feed' | 'keyword'
|
|
tier String // 'minimal' | 'rich'
|
|
// ... other fields
|
|
|
|
@@index([qualityScore])
|
|
@@index([npmDownloadsLastMonth])
|
|
}
|
|
```
|
|
|
|
**`SyncCheckpoint` - Tracks sync progress:**
|
|
```prisma
|
|
model SyncCheckpoint {
|
|
id String @id @default(cuid())
|
|
source String @unique // 'changes-feed' | 'keyword-search' | 'metrics'
|
|
checkpoint Json // { lastSeq: string, lastRun: string, ... }
|
|
}
|
|
```
|
|
|
|
**`SyncLog` - Records all sync operations:**
|
|
```prisma
|
|
model SyncLog {
|
|
id String @id @default(cuid())
|
|
source String
|
|
status String // 'success' | 'partial' | 'error'
|
|
processed Int
|
|
skipped Int
|
|
errors Int
|
|
message String?
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
```
|
|
|
|
### Manual Sync Triggers
|
|
|
|
To manually trigger a sync (useful for testing or debugging):
|
|
|
|
```bash
|
|
# Trigger changes feed sync
|
|
curl -X POST https://tpmjs.com/api/sync/changes \
|
|
-H "Authorization: Bearer $CRON_SECRET"
|
|
|
|
# Trigger keyword search
|
|
curl -X POST https://tpmjs.com/api/sync/keyword \
|
|
-H "Authorization: Bearer $CRON_SECRET"
|
|
|
|
# Trigger metrics update
|
|
curl -X POST https://tpmjs.com/api/sync/metrics \
|
|
-H "Authorization: Bearer $CRON_SECRET"
|
|
```
|
|
|
|
**Note:** You need the `CRON_SECRET` environment variable set in Vercel. The endpoints return 401 Unauthorized without it.
|
|
|
|
### Monitoring Sync Health
|
|
|
|
Check sync logs in the database:
|
|
|
|
```typescript
|
|
// Get recent sync operations
|
|
const recentSyncs = await prisma.syncLog.findMany({
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 20,
|
|
});
|
|
|
|
// Check last successful sync for each source
|
|
const checkpoints = await prisma.syncCheckpoint.findMany();
|
|
```
|
|
|
|
**Sync Log Example:**
|
|
```json
|
|
{
|
|
"id": "clx...",
|
|
"source": "changes-feed",
|
|
"status": "success",
|
|
"processed": 5,
|
|
"skipped": 93,
|
|
"errors": 0,
|
|
"message": "Successfully processed 5 packages",
|
|
"metadata": {
|
|
"durationMs": 2834,
|
|
"lastSeq": "12345678",
|
|
"pending": 1250
|
|
},
|
|
"createdAt": "2025-11-30T12:00:00Z"
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
All sync endpoints follow this error handling pattern:
|
|
|
|
1. **Partial Success:** If some packages fail but others succeed, status is `partial`
|
|
2. **Complete Failure:** If the entire sync fails, status is `error`
|
|
3. **Error Messages:** First 3 errors are included in the response
|
|
4. **Logging:** All operations are logged to `syncLog` regardless of success
|
|
|
|
**Example Partial Failure:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"processed": 5,
|
|
"skipped": 2,
|
|
"errors": 3,
|
|
"durationMs": 5234
|
|
}
|
|
}
|
|
```
|
|
|
|
The sync log will contain:
|
|
```json
|
|
{
|
|
"status": "partial",
|
|
"message": "Processed with errors: Failed to process pkg1: Network timeout; Failed to process pkg2: Invalid tpmjs field; ..."
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
Required environment variables in Vercel:
|
|
|
|
```bash
|
|
# Database connection
|
|
DATABASE_URL="postgresql://..."
|
|
|
|
# Cron job authentication
|
|
CRON_SECRET="your-secret-key"
|
|
```
|
|
|
|
**Important:** Vercel Cron automatically adds the `Authorization: Bearer $CRON_SECRET` header when calling the endpoints. No manual configuration needed.
|
|
|
|
### Performance Considerations
|
|
|
|
**Timeouts:**
|
|
- All sync routes have `maxDuration: 300` (5 minutes)
|
|
- Changes feed processes max 100 packages per run to avoid timeouts
|
|
- Keyword search processes max 250 packages per run
|
|
- Metrics sync processes all tools but runs only once per hour
|
|
|
|
**Rate Limiting:**
|
|
- npm API has rate limits - be cautious when testing manually
|
|
- Vercel Cron jobs run from Vercel's infrastructure (different IP than dev)
|
|
- Consider implementing exponential backoff for npm API errors
|
|
|
|
**Cold Starts:**
|
|
- First request to each sync endpoint may be slow due to Prisma initialization
|
|
- Subsequent requests are faster with warm Prisma Client
|
|
- This is acceptable for background cron jobs
|
|
|
|
### Debugging Sync Issues
|
|
|
|
**Check if cron jobs are running:**
|
|
|
|
```bash
|
|
# View recent deployments
|
|
vercel ls
|
|
|
|
# Check logs for a specific deployment
|
|
vercel logs <deployment-url>
|
|
|
|
# Filter for sync-related logs
|
|
vercel logs <deployment-url> | grep sync
|
|
```
|
|
|
|
**Common issues:**
|
|
|
|
1. **"Unauthorized" errors:** Check that `CRON_SECRET` is set in Vercel environment variables
|
|
2. **Timeouts:** Reduce batch size in changes feed (currently 100)
|
|
3. **Missing packages:** Check `syncLog` for errors during processing
|
|
4. **Stale data:** Verify metrics sync is running every hour
|
|
|
|
**Test sync locally:**
|
|
|
|
```bash
|
|
# Start dev server
|
|
pnpm dev --filter=@tpmjs/web
|
|
|
|
# Trigger sync (requires CRON_SECRET in .env.local)
|
|
curl -X POST http://localhost:3000/api/sync/changes \
|
|
-H "Authorization: Bearer $CRON_SECRET"
|
|
```
|
|
|
|
### Package Discovery Flow
|
|
|
|
Here's how a new TPMJS tool gets discovered:
|
|
|
|
1. **Developer publishes package to npm** with `tpmjs` keyword and `tpmjs` field in package.json
|
|
2. **Within 2 minutes:** Changes feed sync picks it up from npm's `/_changes` endpoint
|
|
3. **Validation:** `validateTpmjsField()` checks that the `tpmjs` field meets requirements
|
|
4. **Database Insert:** Tool is upserted with initial data
|
|
5. **Within 1 hour:** Metrics sync updates download stats and calculates quality score
|
|
6. **Visible on tpmjs.com:** Tool appears in search results and category pages
|
|
|
|
**Backup Discovery:** If changes feed misses a package, the keyword search (every 15 minutes) will catch it.
|
|
|
|
### Future Improvements
|
|
|
|
Potential enhancements to the sync system:
|
|
|
|
- [ ] Add webhook endpoint for instant npm package notifications
|
|
- [ ] Implement exponential backoff for npm API rate limits
|
|
- [ ] Add Slack/Discord notifications for sync failures
|
|
- [ ] Create admin dashboard to monitor sync health
|
|
- [ ] Support GitHub stars syncing (requires GitHub API integration)
|
|
- [ ] Add sync metrics to Vercel Analytics
|
|
- [ ] Implement differential sync to reduce database writes
|
|
|
|
---
|
|
|
|
## Verifying Production Deployments
|
|
|
|
To verify that a deployment has been successfully pushed to production, fetch the `/api/health` endpoint:
|
|
|
|
```bash
|
|
curl https://tpmjs.com/api/health
|
|
```
|
|
|
|
**Response format:**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"timestamp": "2025-01-07T13:30:00.000Z",
|
|
"build": {
|
|
"commitSha": "efb2802",
|
|
"commitMessage": "feat: add public chat page for agents",
|
|
"deploymentUrl": "tpmjs-xyz123.vercel.app"
|
|
},
|
|
"env": {
|
|
"hasDatabase": true,
|
|
"nodeEnv": "production"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key fields:**
|
|
- `commitSha` - The short git commit hash (7 chars) of the deployed code
|
|
- `commitMessage` - The commit message of the deployed commit
|
|
- `deploymentUrl` - The Vercel deployment URL
|
|
|
|
**Verification workflow:**
|
|
1. After pushing, check the latest commit: `git log --oneline -1`
|
|
2. Wait for CI and Vercel deployment to complete
|
|
3. Fetch `/api/health` and compare `commitSha` with your local commit
|
|
4. If they match, the deployment is live
|
|
|
|
**Note:** Vercel provides these values via environment variables (`VERCEL_GIT_COMMIT_SHA`, `VERCEL_GIT_COMMIT_MESSAGE`, `VERCEL_URL`) which are automatically available at runtime. |