Loading...
Loading...
Follow Recovery Coach codebase patterns and conventions. Use when writing new code, components, API routes, or database queries. Activates for general development, code organization, styling, and architectural decisions in this project.
npx skill4agent add erichowens/some_claude_skills recovery-coach-patternscrisis-response-protocolmodern-drug-rehab-computerrecovery-community-moderatorsrc/
├── app/ # Next.js App Router
│ ├── api/ # API routes (REST endpoints)
│ │ ├── auth/ # Authentication endpoints
│ │ ├── check-in/ # Daily check-in endpoints
│ │ ├── chat/ # AI coaching endpoints
│ │ └── admin/ # Admin-only endpoints
│ ├── admin/ # Admin dashboard page
│ ├── settings/ # User settings page
│ └── page.tsx # Home page
├── components/ # React components
│ ├── ui/ # Base UI components (Button, Card, etc.)
│ └── *.tsx # Feature components
├── lib/ # Core business logic
│ ├── ai/ # Anthropic integration
│ ├── hipaa/ # HIPAA compliance utilities
│ ├── auth.ts # Authentication
│ ├── db.ts # Database connection
│ └── rate-limit.ts # Rate limiting
├── db/ # Database schema (Drizzle ORM)
│ ├── schema.ts # Table definitions
│ └── secure-db.ts # RLS-enforced queries
└── test/ # Test utilities
features/ # Feature manifests (YAML)
docs/ # Documentation
scripts/ # Build and utility scripts// src/app/api/[feature]/route.ts
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { createRateLimiter } from '@/lib/rate-limit';
import { logPHIAccess } from '@/lib/hipaa/audit';
import { z } from 'zod';
// 1. Define rate limiter
const rateLimiter = createRateLimiter({
windowMs: 60000,
maxRequests: 30,
keyPrefix: 'api:feature'
});
// 2. Define input schema
const RequestSchema = z.object({
field: z.string().min(1).max(1000),
});
export async function POST(request: Request) {
// 3. Check authentication
const session = await getSession();
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 4. Apply rate limiting
const rateLimitResult = await rateLimiter.check(session.userId);
if (!rateLimitResult.allowed) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429, headers: rateLimitResult.headers }
);
}
// 5. Parse and validate input
const body = await request.json();
const parsed = RequestSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Invalid input', details: parsed.error.issues },
{ status: 400 }
);
}
// 6. Perform operation
const result = await performOperation(session.userId, parsed.data);
// 7. Audit log (if PHI)
await logPHIAccess(session.userId, 'feature', result.id, 'CREATE');
// 8. Return response
return NextResponse.json(result);
}// src/components/FeatureComponent.tsx
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
interface FeatureProps {
id: string;
initialData?: FeatureData;
onComplete?: (result: Result) => void;
}
export function FeatureComponent({
id,
initialData,
onComplete
}: FeatureProps) {
const [data, setData] = useState<FeatureData | null>(initialData ?? null);
const [loading, setLoading] = useState(!initialData);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!initialData) {
fetchData();
}
}, [id]);
async function fetchData() {
try {
setLoading(true);
const res = await fetch(`/api/feature/${id}`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
setData(data);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setLoading(false);
}
}
if (loading) {
return <div className="animate-pulse">Loading...</div>;
}
if (error) {
return (
<Card className="border-destructive">
<CardContent>Error: {error}</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>Feature Title</CardHeader>
<CardContent>
{/* Content */}
</CardContent>
</Card>
);
}// Use secure-db for user data (RLS enforced)
import { db, users, checkIns } from '@/db/secure-db';
import { eq, desc } from 'drizzle-orm';
// Get user's own data (RLS automatically filters)
async function getUserCheckIns(userId: string) {
return db
.select()
.from(checkIns)
.where(eq(checkIns.userId, userId))
.orderBy(desc(checkIns.createdAt))
.limit(30);
}
// For admin queries, use requireAdmin
import { requireAdmin } from '@/db/secure-db';
async function getAdminStats() {
const admin = await requireAdmin();
if (!admin) throw new Error('Admin required');
// Now can query across all users
return db.select({ count: count() }).from(users);
}/* From globals.css */
--navy: #1a365d; /* Primary - trust, stability */
--teal: #319795; /* Secondary - calm, healing */
--coral: #ed8936; /* Accent - warmth, energy */
--cream: #fffaf0; /* Background - comfort */function getTimeTheme(): 'dawn' | 'day' | 'dusk' | 'night' {
const hour = new Date().getHours();
if (hour >= 5 && hour < 9) return 'dawn';
if (hour >= 9 && hour < 17) return 'day';
if (hour >= 17 && hour < 21) return 'dusk';
return 'night';
}// Use Tailwind with design tokens
<Button
className="bg-navy hover:bg-navy/90 text-white"
variant="default"
>
Primary Action
</Button>
<Card className="bg-cream border-teal/20">
<CardContent className="text-navy">
Therapeutic content
</CardContent>
</Card>// src/lib/__tests__/feature.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Feature', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('loads and displays data', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ data: 'test' })
} as Response);
render(<FeatureComponent id="123" />);
await waitFor(() => {
expect(screen.getByText('test')).toBeInTheDocument();
});
});
it('handles errors gracefully', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
render(<FeatureComponent id="123" />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});// Use structured error responses
interface APIError {
error: string;
code?: string;
details?: unknown;
}
// In API routes
return NextResponse.json<APIError>(
{
error: 'Validation failed',
code: 'VALIDATION_ERROR',
details: zodError.issues
},
{ status: 400 }
);
// In components
try {
await submitData();
} catch (e) {
if (e instanceof APIError) {
toast.error(e.message);
} else {
toast.error('An unexpected error occurred');
console.error(e); // Log for debugging, not shown to user
}
}# Authentication
SESSION_SECRET= # 32+ random characters
# AI Integration
ANTHROPIC_API_KEY= # Claude API key
# Database
DATABASE_URL= # SQLite path or connection string
# Optional
VAPID_PUBLIC_KEY= # Push notifications
VAPID_PRIVATE_KEY=npm run lintnpm run testnpm run feature:validate// Authentication
import { getSession, requireAuth } from '@/lib/auth';
// Database
import { db } from '@/db/secure-db';
import { eq, desc, and } from 'drizzle-orm';
// Audit
import { logPHIAccess, logSecurityEvent } from '@/lib/hipaa/audit';
// Rate limiting
import { createRateLimiter } from '@/lib/rate-limit';
// Validation
import { z } from 'zod';
// UI Components
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';