Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:47:35 +08:00
commit b90acffddf
9 changed files with 2356 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "carebridge-standards",
"description": "Comprehensive coding standards and architectural patterns for the CareBridge eldercare management application. Includes critical patterns for case context management, Stripe integration, Next.js 15 requirements, component creation rules, and database operations.",
"version": "1.0.0",
"author": {
"name": "William VanSickle III"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# carebridge-standards
Comprehensive coding standards and architectural patterns for the CareBridge eldercare management application. Includes critical patterns for case context management, Stripe integration, Next.js 15 requirements, component creation rules, and database operations.

288
SKILL.md Normal file
View File

@@ -0,0 +1,288 @@
---
name: carebridge-standards
description: Comprehensive coding standards and architectural patterns for the CareBridge eldercare management application. Includes critical patterns for case context management, Stripe integration, Next.js 15 requirements, component creation rules, and database operations. This skill should be used whenever working on the CareBridge codebase to ensure consistency and prevent common mistakes.
---
# CareBridge Development Standards
This skill provides comprehensive coding standards, architectural patterns, and best practices for developing the CareBridge application - a multi-case eldercare management SaaS platform.
## Overview
CareBridge has specific architectural patterns that MUST be followed to ensure:
- **Case context preservation** across all navigation
- **Consistent Stripe integration** for SaaS subscriptions and concierge packages
- **Next.js 15 compliance** with async request APIs
- **UI consistency** using shadcn/ui components only
- **Database security** with proper RLS policies and migrations
Breaking these patterns causes critical bugs, build errors, and scalability issues.
## When to Use This Skill
**ALWAYS use this skill when:**
- Adding new features to CareBridge
- Fixing bugs in the CareBridge codebase
- Creating new pages or components
- Implementing navigation or routing
- Working with Stripe payments
- Creating database migrations
- Reviewing or refactoring code
**Critical scenarios that require this skill:**
- Any page that displays case-specific data
- Any navigation component or link
- Any Stripe checkout or webhook code
- Any new UI component creation
- Any database schema changes
## Core Standards
### 🚨 Rule #1: NEVER Break Case Context
**Every navigation link, redirect, or router.push MUST preserve the `caseId` query parameter.**
Bad example:
```typescript
<Link href="/dashboard">Dashboard</Link>
```
Good example:
```typescript
const caseId = searchParams.get('caseId')
<Link href={caseId ? `/dashboard?caseId=${caseId}` : '/dashboard'}>Dashboard</Link>
```
See `references/case-context.md` for complete patterns.
### 🚨 Rule #2: NEVER Create Custom Components
**Always use shadcn/ui. Never create custom UI components.**
Bad example:
```typescript
export function CustomButton({ children }) {
return <button className="custom-btn">{children}</button>
}
```
Good example:
```typescript
import { Button } from '@/components/ui/button'
<Button variant="default">{children}</Button>
```
Add components with: `npx shadcn@latest add button`
See `references/component-standards.md` for the complete list of available components.
### 🚨 Rule #3: ALWAYS Await Params in Next.js 15
**Next.js 15 requires awaiting params and searchParams.**
Bad example:
```typescript
export default function Page({ params }: { params: { id: string } }) {
const { id } = params // ERROR!
}
```
Good example:
```typescript
type Props = { params: Promise<{ id: string }> }
export default async function Page({ params }: Props) {
const { id } = await params
}
```
See `references/nextjs-15-patterns.md` for all Next.js 15 changes.
### 🚨 Rule #4: ALWAYS Use Metadata in Stripe Checkouts
**Stripe webhooks route based on metadata.package_type.**
Bad example:
```typescript
const session = await stripe.checkout.sessions.create({
line_items: [...],
mode: 'subscription',
})
```
Good example:
```typescript
const session = await stripe.checkout.sessions.create({
line_items: [...],
mode: 'subscription',
metadata: {
clerk_user_id: userId,
package_type: 'concierge_plus',
payment_type: 'subscription',
},
})
```
See `references/stripe-integration.md` for the complete integration guide.
### 🚨 Rule #5: ALWAYS Use Supabase CLI for Migrations
**Never create tables via SQL in application code.**
Bad example:
```typescript
await supabase.sql`CREATE TABLE ...` // Don't do this!
```
Good example:
```bash
npx supabase migration new create_table
# Edit the generated .sql file
npx supabase db push
```
See `references/database-patterns.md` for migration workflows.
## Implementation Workflow
When implementing any feature in CareBridge, follow this workflow:
### Step 1: Identify the Domain
Before writing any code, identify which domain(s) you're working in:
- **Navigation/Routing** → Read `references/case-context.md`
- **Payments/Subscriptions** → Read `references/stripe-integration.md`
- **Pages/API Routes** → Read `references/nextjs-15-patterns.md`
- **UI Components** → Read `references/component-standards.md`
- **Database Operations** → Read `references/database-patterns.md`
### Step 2: Follow the Patterns
Each reference document contains:
-**CORRECT** patterns to follow
-**WRONG** anti-patterns to avoid
- Executable code examples
- Common mistakes section
- Checklists
### Step 3: Validate Against Standards
Before completing any task, verify:
**Case Context Checklist:**
- [ ] All navigation links preserve `caseId` query parameter
- [ ] Page components await params in Next.js 15
- [ ] Client components use `useEffect` for `caseId` to prevent hydration mismatch
- [ ] Empty states shown when `caseId` is missing
**Stripe Integration Checklist:**
- [ ] Pricing uses `src/lib/config/concierge-pricing.ts` as single source of truth
- [ ] Checkout sessions include proper metadata for webhook routing
- [ ] Webhook handlers check `metadata.package_type` for routing
- [ ] Price validation happens before checkout creation
**Next.js 15 Checklist:**
- [ ] All `params` are typed as `Promise<{ ... }>` and awaited
- [ ] All `searchParams` are typed as `Promise<{ ... }>` and awaited
- [ ] 'use client' only added when needed (hooks, interactivity, browser APIs)
- [ ] Server components used for data fetching by default
**Component Standards Checklist:**
- [ ] Using shadcn/ui components (added via `npx shadcn@latest add`)
- [ ] NO custom UI components created
- [ ] NO custom CSS variables defined
- [ ] Using Tailwind classes instead of inline styles
- [ ] Proper TypeScript types defined
**Database Patterns Checklist:**
- [ ] Migrations created via `npx supabase migration new`
- [ ] RLS enabled on all tables
- [ ] RLS policies created for SELECT, INSERT, UPDATE, DELETE
- [ ] Indexes added for frequently queried columns
- [ ] Using `createServerClient()` in server components
- [ ] Using `createBrowserClient()` in client components
## Common Mistakes to Avoid
Based on real bugs encountered during development:
1. **Losing case context** - Navigation without preserving caseId
2. **Not awaiting params** - Using Next.js 14 patterns in Next.js 15
3. **Creating custom components** - Instead of using shadcn/ui
4. **Defining custom CSS variables** - Instead of using existing ones
5. **Forgetting RLS policies** - Creating tables without Row-Level Security
6. **Skipping migrations** - Creating tables in application code
7. **Missing webhook metadata** - Webhooks unable to route correctly
8. **Using client Supabase in server** - Wrong client type for component
## Project Context
**Application**: CareBridge - Multi-case eldercare management platform
**Tech Stack**:
- Next.js 15.5.4 with App Router
- React 19.1.0
- TypeScript 5.x
- Supabase (PostgreSQL with RLS)
- Clerk (Authentication)
- Stripe (Payments)
- shadcn/ui (Components)
- Tailwind CSS (Styling)
**Architecture**:
- Multi-tenant: Users can manage multiple care cases
- Case-centric: All data scoped to caseId
- SaaS subscription: $19.99/month platform access
- Concierge packages: 4 service tiers (one-time, project-based, subscription)
## Resources
This skill includes comprehensive reference documentation in the `references/` directory:
### references/
1. **`case-context.md`**
- How caseId preservation works
- Reading case context in server/client components
- Navigation patterns
- Common mistakes and fixes
2. **`stripe-integration.md`**
- Pricing configuration (single source of truth)
- Creating checkout sessions
- Webhook handling and metadata routing
- Database structure
- Testing checklist
3. **`nextjs-15-patterns.md`**
- Async params and searchParams
- Server vs client components
- Data fetching patterns
- Metadata API
- Common migration errors
4. **`component-standards.md`**
- Using shadcn/ui components
- CSS variables (read-only)
- Form patterns with React Hook Form + Zod
- Dialog/modal patterns
- Loading and empty states
5. **`database-patterns.md`**
- Supabase client patterns
- Migration creation and structure
- RLS policies and patterns
- Query patterns (CRUD, joins, filtering)
- Transaction patterns with RPC
- Error handling
## Getting Help
When stuck, refer to the reference documents for:
- Copy-paste ready code examples
- Step-by-step checklists
- Anti-patterns to avoid
- Testing procedures
Remember: These patterns exist because they've been proven through real development. Breaking them leads to bugs, build errors, and technical debt.

65
plugin.lock.json Normal file
View File

@@ -0,0 +1,65 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:Human-Frontier-Labs-Inc/human-frontier-labs-marketplace:plugins/carebridge-standards",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "6c2c85b3cf524cfc76ea5c2f663d262f2c3eb35c",
"treeHash": "06aa3af1e3376df65c906283044132fa68776be103eaf224efb6686d9ec35e28",
"generatedAt": "2025-11-28T10:11:40.591737Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "carebridge-standards",
"description": "Comprehensive coding standards and architectural patterns for the CareBridge eldercare management application. Includes critical patterns for case context management, Stripe integration, Next.js 15 requirements, component creation rules, and database operations.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d45c0b482ad42fbd4228d8ce15b51fcaed279eb1e1e6f805f2b01e5b7ac9a81d"
},
{
"path": "SKILL.md",
"sha256": "4427546d719a5d94b0447d417d6320f7c590ec8dbc6b5f7f066a945432ce507e"
},
{
"path": "references/nextjs-15-patterns.md",
"sha256": "fa46459461fda029c374bd91f3d03ab953555fc00e108c3c6f81b5edf4809257"
},
{
"path": "references/stripe-integration.md",
"sha256": "904c2e65b92a44f972d59d1028db779f27f155dfc3775ba802f7ce8dc126dc54"
},
{
"path": "references/component-standards.md",
"sha256": "4388aeeab5ee93df502da8c4bb48561d2af7f7f2f69e8d1b23d89be6985c442d"
},
{
"path": "references/case-context.md",
"sha256": "7bc7e4336905c69b25451ad503b552c007bb2c7132a6eba56584155091323a11"
},
{
"path": "references/database-patterns.md",
"sha256": "4e835a8fc93b1fa7171e3dcead8213a760b54834daf171446329636692c09d75"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "b72b997321dacca515475700f3ba7a4fae9a1bed32427c21509b43f8b441c2f3"
}
],
"dirSha256": "06aa3af1e3376df65c906283044132fa68776be103eaf224efb6686d9ec35e28"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

189
references/case-context.md Normal file
View File

@@ -0,0 +1,189 @@
# Case Context Management - CareBridge
## Overview
CareBridge is a multi-case management system where users can manage multiple care recipients. The `caseId` is the primary context identifier that must be preserved across ALL navigation.
## Critical Rule
**NEVER navigate to a page without preserving the current `caseId`.**
## How Case Context Works
### URL Structure
All protected routes must support the `caseId` query parameter:
```
/dashboard?caseId=uuid-here
/calendar?caseId=uuid-here
/subscriptions?caseId=uuid-here
/case-settings?caseId=uuid-here
```
### Reading Case Context
Use the helper function to safely extract `caseId`:
```typescript
// In Server Components (pages)
import { getCaseIdFromParams } from '@/lib/case-context'
type SearchParams = Promise<{ caseId?: string }>
export default async function MyPage({
searchParams,
}: {
searchParams: SearchParams
}) {
const params = await searchParams // Next.js 15 requirement
const caseId = getCaseIdFromParams(params)
if (!caseId) {
// Show "No case selected" empty state
return <EmptyState />
}
// Use caseId for data fetching
const data = await getData(caseId)
// ...
}
```
```typescript
// In Client Components
'use client'
import { useSearchParams } from 'next/navigation'
import { useState, useEffect } from 'react'
export function MyClientComponent() {
const searchParams = useSearchParams()
const [caseId, setCaseId] = useState<string | null>(null)
// Prevent hydration mismatch
useEffect(() => {
setCaseId(searchParams.get('caseId'))
}, [searchParams])
if (!caseId) return null
// Use caseId
}
```
### Preserving Case Context in Navigation
**All navigation links MUST preserve caseId:**
```typescript
// ✅ CORRECT - Preserves caseId
import { useSearchParams } from 'next/navigation'
import Link from 'next/link'
const searchParams = useSearchParams()
const caseId = searchParams.get('caseId')
<Link href={caseId ? `/dashboard?caseId=${caseId}` : '/dashboard'}>
Dashboard
</Link>
// ❌ WRONG - Loses case context
<Link href="/dashboard">
Dashboard
</Link>
```
### Navigation Component Pattern
See `src/components/nav-main.tsx` for the correct pattern:
```typescript
'use client'
export function NavMain({ items }) {
const searchParams = useSearchParams()
const [caseId, setCaseId] = useState<string | null>(null)
useEffect(() => {
setCaseId(searchParams.get('caseId'))
}, [searchParams])
return items.map((item) => {
// Preserve caseId in ALL navigation
const url = caseId ? `${item.url}?caseId=${caseId}` : item.url
return <Link href={url}>{item.title}</Link>
})
}
```
## Case Switching
Case switching happens in the sidebar via `CaseSwitcher` component. When a user selects a different case:
1. The URL updates with the new `caseId`
2. All navigation automatically preserves the new `caseId`
3. The page re-renders with new case data
## Empty States
When `caseId` is missing, show a helpful empty state:
```typescript
if (!caseId) {
return (
<EmptyState
icon={Users}
title="No Case Selected"
description="Select a case from the sidebar to continue"
/>
)
}
```
## Common Mistakes
### ❌ Mistake #1: Forgetting to preserve caseId in links
```typescript
// WRONG - Will break case context
<Link href="/subscriptions">Subscribe</Link>
```
### ❌ Mistake #2: Not handling Next.js 15 async params
```typescript
// WRONG - In Next.js 15, params are async
export default function Page({ searchParams }) {
const caseId = searchParams.caseId // ERROR!
}
// CORRECT
export default async function Page({ searchParams }) {
const params = await searchParams
const caseId = getCaseIdFromParams(params)
}
```
### ❌ Mistake #3: Using router.push without caseId
```typescript
// WRONG
router.push('/dashboard')
// CORRECT
const caseId = searchParams.get('caseId')
router.push(caseId ? `/dashboard?caseId=${caseId}` : '/dashboard')
```
## Testing Case Context
When testing a feature:
1. Navigate to a page with a caseId
2. Click any link/button that navigates
3. Verify the URL still has `?caseId=...`
4. Repeat for all navigation paths
If caseId is lost, you'll see the "No Case Selected" empty state - this indicates broken context preservation.

View File

@@ -0,0 +1,435 @@
# Component Standards - CareBridge
## Critical Rule: No Custom Components
**NEVER create custom UI components from scratch. ALWAYS use shadcn/ui.**
## Using shadcn/ui
### Adding Components
```bash
# List available components
npx shadcn@latest add
# Add a specific component
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog
npx shadcn@latest add form
```
This adds components to `src/components/ui/` with proper TypeScript types and styling.
### Example: Adding a Button
```bash
npx shadcn@latest add button
```
Then use it:
```typescript
import { Button } from '@/components/ui/button'
export function MyComponent() {
return <Button variant="default">Click me</Button>
}
```
### Available Variants
shadcn components come with built-in variants. Check the component file for options:
```typescript
// Button variants
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Card components
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>
Content here
</CardContent>
<CardFooter>
Footer actions
</CardFooter>
</Card>
```
## Component Organization
### Location
```
src/
├── components/
│ ├── ui/ # shadcn components (DON'T EDIT)
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── ...
│ ├── dashboard/ # Dashboard-specific components
│ ├── calendar/ # Calendar-specific components
│ ├── concierge/ # Concierge-specific components
│ └── nav-main.tsx # Global navigation components
```
### Naming Conventions
- UI components: `kebab-case.tsx` (button.tsx, card.tsx)
- Feature components: `PascalCase.tsx` (DashboardHeader.tsx, CaseList.tsx)
- Use descriptive names: `ConciergeBookingForm.tsx` not `Form.tsx`
## Styling Rules
### CSS Variables (READ-ONLY)
**NEVER create custom CSS variables. ONLY use existing ones from `globals.css`.**
Available CSS variables:
```css
/* From globals.css */
--background
--foreground
--card
--card-foreground
--popover
--popover-foreground
--primary
--primary-foreground
--secondary
--secondary-foreground
--muted
--muted-foreground
--accent
--accent-foreground
--destructive
--destructive-foreground
--border
--input
--ring
```
### Using CSS Variables
```typescript
// ✅ CORRECT - Use existing variables
<div className="bg-background text-foreground">
Content
</div>
// ✅ CORRECT - Use Tailwind utility classes
<div className="bg-primary text-primary-foreground rounded-lg p-4">
Card content
</div>
// ❌ WRONG - Custom CSS variables
<div style={{ backgroundColor: 'var(--my-custom-color)' }}>
Content
</div>
// ❌ WRONG - Inline styles
<div style={{ backgroundColor: '#3498db', padding: '16px' }}>
Content
</div>
```
### Tailwind Classes
Use Tailwind utility classes for styling:
```typescript
// ✅ CORRECT
<div className="flex items-center gap-4 p-6 rounded-lg border">
<div className="flex-1">Content</div>
<Button variant="default">Action</Button>
</div>
// ❌ WRONG - Custom CSS classes
<div className="my-custom-container">
Content
</div>
```
## Form Components
### Using React Hook Form + Zod
CareBridge uses React Hook Form with Zod validation:
```typescript
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
const formSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
})
export function MyForm() {
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
},
})
const onSubmit = async (values: z.infer<typeof formSchema>) => {
// Handle submission
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
```
## Dialog/Modal Pattern
```typescript
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
export function MyDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
</DialogHeader>
<div>Dialog content here</div>
</DialogContent>
</Dialog>
)
}
```
## Empty States Pattern
Use the `EmptyState` component for "no data" scenarios:
```typescript
import { EmptyState } from '@/components/dashboard/empty-state'
import { Users } from 'lucide-react'
export function MyComponent({ data }) {
if (!data || data.length === 0) {
return (
<EmptyState
icon={Users}
title="No Cases Found"
description="Create your first care case to get started"
/>
)
}
return <div>{/* Render data */}</div>
}
```
## Loading States
```typescript
import { Skeleton } from '@/components/ui/skeleton'
export function LoadingState() {
return (
<div className="space-y-4">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
)
}
```
## Icons
Use Lucide React for icons:
```typescript
import { Calendar, User, Settings, CreditCard } from 'lucide-react'
export function MyComponent() {
return (
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
<span>Calendar</span>
</div>
)
}
```
## Common Component Patterns
### Card with Actions
```typescript
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Title</CardTitle>
<Button variant="ghost" size="sm">
<Settings className="h-4 w-4" />
</Button>
</div>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>
Content here
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>
```
### List with Items
```typescript
<div className="space-y-2">
{items.map((item) => (
<Card key={item.id}>
<CardContent className="flex items-center justify-between p-4">
<div>
<h3 className="font-medium">{item.title}</h3>
<p className="text-sm text-muted-foreground">{item.description}</p>
</div>
<Button variant="ghost" size="sm">
View
</Button>
</CardContent>
</Card>
))}
</div>
```
## Responsive Design
Use Tailwind responsive prefixes:
```typescript
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Responsive grid */}
</div>
<div className="flex flex-col sm:flex-row gap-4">
{/* Stack vertically on mobile, horizontally on larger screens */}
</div>
```
## Common Mistakes
### ❌ Mistake #1: Creating custom components
```typescript
// WRONG - Don't create custom buttons
export function CustomButton({ children }) {
return (
<button className="custom-btn">
{children}
</button>
)
}
// CORRECT - Use shadcn button
import { Button } from '@/components/ui/button'
<Button variant="default">{children}</Button>
```
### ❌ Mistake #2: Custom CSS variables
```typescript
// WRONG - Don't create custom variables
<div style={{ color: 'var(--my-custom-color)' }}>
Content
</div>
// CORRECT - Use existing variables
<div className="text-primary">
Content
</div>
```
### ❌ Mistake #3: Inline styles
```typescript
// WRONG - Avoid inline styles
<div style={{ padding: '16px', margin: '8px' }}>
Content
</div>
// CORRECT - Use Tailwind classes
<div className="p-4 m-2">
Content
</div>
```
### ❌ Mistake #4: Not using TypeScript types
```typescript
// WRONG - No types
export function MyComponent({ user }) {
return <div>{user.name}</div>
}
// CORRECT - Proper TypeScript types
type Props = {
user: {
name: string
email: string
}
}
export function MyComponent({ user }: Props) {
return <div>{user.name}</div>
}
```
## Component Checklist
Before creating a component:
- [ ] Is there a shadcn component for this? (Use `npx shadcn@latest add`)
- [ ] Am I using existing CSS variables?
- [ ] Am I using Tailwind classes instead of inline styles?
- [ ] Do I have proper TypeScript types?
- [ ] Is the component in the right directory?
- [ ] Am I preserving case context in navigation?

View File

@@ -0,0 +1,588 @@
# Database Patterns - CareBridge
## Overview
CareBridge uses Supabase (PostgreSQL) for data storage. Security is enforced via **Clerk authentication + Server Actions** for most tables, with RLS enabled only for specific tables (user_profiles, consultations, subscriptions).
## Database Schema Reference
### Auto-Generated Types (Single Source of Truth)
The complete database schema is auto-generated in **`src/lib/database.types.ts`**.
**To regenerate types after migrations:**
```bash
# From linked remote project (recommended)
npx supabase gen types typescript --linked > src/lib/database.types.ts
# From local dev database
npx supabase gen types typescript --local > src/lib/database.types.ts
# From specific project ID
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/lib/database.types.ts
```
**ALWAYS regenerate types after creating migrations!**
## Supabase Client Patterns
### Server-Side Client (Preferred)
Use in Server Components and Server Actions:
```typescript
import { createServerClient } from '@/lib/supabase/server'
export default async function Page() {
const supabase = await createServerClient()
const { data, error } = await supabase
.from('cases')
.select('*')
.eq('owner_id', userId)
if (error) {
console.error('Database error:', error)
return <ErrorDisplay />
}
return <CasesList cases={data} />
}
```
### Client-Side Client (When Needed)
Use in Client Components:
```typescript
'use client'
import { createBrowserClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
export function ClientComponent() {
const [data, setData] = useState(null)
const supabase = createBrowserClient()
useEffect(() => {
async function fetchData() {
const { data } = await supabase.from('cases').select('*')
setData(data)
}
fetchData()
}, [])
// ...
}
```
## Migrations
### Creating Migrations
**ALWAYS use Supabase CLI for schema changes:**
```bash
# Create a new migration
npx supabase migration new migration_name
# This creates: supabase/migrations/YYYYMMDD_migration_name.sql
```
### Migration Structure
```sql
-- Migration: 20251018_add_concierge_packages.sql
-- Create table
CREATE TABLE IF NOT EXISTS concierge_packages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clerk_user_id TEXT NOT NULL,
package_type VARCHAR(50) NOT NULL,
price_paid_cents INT NOT NULL CHECK (price_paid_cents > 0),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Create indexes
CREATE INDEX idx_concierge_packages_user
ON concierge_packages(clerk_user_id);
CREATE INDEX idx_concierge_packages_status
ON concierge_packages(status);
-- Add table comment
COMMENT ON TABLE concierge_packages IS 'Stores concierge service package purchases. Security enforced by Clerk auth + Server Actions (RLS disabled)';
-- Create trigger for updated_at (if function exists)
CREATE TRIGGER update_concierge_packages_updated_at
BEFORE UPDATE ON concierge_packages
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Add column comments
COMMENT ON COLUMN concierge_packages.price_paid_cents IS 'Price paid in cents (e.g., 39900 = $399.00)';
```
### Applying Migrations
```bash
# Apply locally
npx supabase db reset
# Apply to remote (production)
npx supabase db push
```
## Security Architecture
### CareBridge Uses Two Security Patterns:
**Pattern 1: Clerk Authentication + Server Actions (Most Tables)**
Most tables have RLS **disabled** and rely on:
- Clerk authentication for user identity
- Server Actions that enforce authorization logic
- Server-side validation before database operations
```sql
-- Example: Most tables use this pattern
CREATE TABLE cases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id TEXT NOT NULL,
-- ... other columns
);
-- RLS is DISABLED - security enforced in Server Actions
COMMENT ON TABLE cases IS 'Security enforced by Clerk auth + Server Actions (RLS disabled)';
```
**Pattern 2: Row-Level Security (Specific Tables Only)**
Only 3 tables use RLS:
- `user_profiles` - User profile data
- `consultations` - Consultation bookings
- `subscriptions` - Subscription data
```sql
-- Example: Tables with RLS enabled
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_own_profile"
ON user_profiles
FOR ALL
USING (auth.jwt() ->> 'sub' = clerk_user_id);
```
### When to Use Each Pattern
**Use Clerk + Server Actions (default):**
- Complex authorization logic (team access, roles)
- Multi-table operations requiring consistency
- Business logic validation
- Most application features
**Use RLS (rare):**
- Simple user-owned resources
- Direct client queries (rare in Next.js)
- Additional security layer for sensitive data
### Server Action Security Pattern
```typescript
// src/lib/actions/case-actions.ts
'use server'
import { auth } from '@clerk/nextjs/server'
import { createServerClient } from '@/lib/supabase/server'
export async function getCases() {
const { userId } = await auth()
if (!userId) {
throw new Error('Unauthorized')
}
const supabase = await createServerClient()
// Security: Only fetch cases owned by authenticated user
const { data, error } = await supabase
.from('cases')
.select('*')
.eq('owner_id', userId)
if (error) throw error
return data
}
```
## Query Patterns
### Basic CRUD
```typescript
// Create
const { data, error } = await supabase
.from('cases')
.insert({
owner_id: userId,
care_recipient_name: 'John Doe',
})
.select()
.single()
// Read (single)
const { data, error } = await supabase
.from('cases')
.select('*')
.eq('id', caseId)
.single()
// Read (multiple)
const { data, error } = await supabase
.from('cases')
.select('*')
.eq('owner_id', userId)
// Update
const { data, error } = await supabase
.from('cases')
.update({ care_recipient_name: 'Jane Doe' })
.eq('id', caseId)
// Delete
const { data, error } = await supabase
.from('cases')
.delete()
.eq('id', caseId)
```
### Joins (Relations)
```typescript
// Get cases with their members
const { data, error } = await supabase
.from('cases')
.select(`
*,
case_members (
id,
user_id,
role
)
`)
.eq('owner_id', userId)
```
### Filtering
```typescript
// Equals
.eq('status', 'active')
// Not equals
.neq('status', 'deleted')
// Greater than
.gt('created_at', '2024-01-01')
// In array
.in('status', ['active', 'pending'])
// Is null
.is('deleted_at', null)
// Like (case-insensitive)
.ilike('name', '%john%')
// Order
.order('created_at', { ascending: false })
// Limit
.limit(10)
// Range (pagination)
.range(0, 9) // First 10 items
```
### Counting
```typescript
const { count, error } = await supabase
.from('cases')
.select('*', { count: 'exact', head: true })
.eq('owner_id', userId)
```
### Upsert (Insert or Update)
```typescript
const { data, error } = await supabase
.from('subscriptions')
.upsert({
clerk_user_id: userId,
is_active: true,
})
.select()
```
## Transaction Patterns
### Using RPC for Transactions
Create a database function:
```sql
-- supabase/migrations/20251018_create_case_with_member.sql
CREATE OR REPLACE FUNCTION create_case_with_member(
p_owner_id TEXT,
p_case_name TEXT
) RETURNS UUID AS $$
DECLARE
v_case_id UUID;
BEGIN
-- Insert case
INSERT INTO cases (owner_id, care_recipient_name)
VALUES (p_owner_id, p_case_name)
RETURNING id INTO v_case_id;
-- Insert owner as member
INSERT INTO case_members (case_id, user_id, role)
VALUES (v_case_id, p_owner_id, 'owner');
RETURN v_case_id;
END;
$$ LANGUAGE plpgsql;
```
Call from TypeScript:
```typescript
const { data, error } = await supabase.rpc('create_case_with_member', {
p_owner_id: userId,
p_case_name: 'John Doe',
})
```
## Error Handling
```typescript
const { data, error } = await supabase
.from('cases')
.select('*')
.eq('id', caseId)
.single()
if (error) {
console.error('Database error:', error)
// Check for specific error codes
if (error.code === 'PGRST116') {
// Not found
return { error: 'Case not found' }
}
return { error: 'Failed to fetch case' }
}
return { data }
```
## Type Safety with Auto-Generated Types
CareBridge uses auto-generated types from `src/lib/database.types.ts`:
```typescript
import { Database } from '@/lib/database.types'
// Extract table types
type Case = Database['public']['Tables']['cases']['Row']
type CaseInsert = Database['public']['Tables']['cases']['Insert']
type CaseUpdate = Database['public']['Tables']['cases']['Update']
// Use with Supabase client
const supabase = createServerClient<Database>()
// Typed queries
const { data, error } = await supabase
.from('cases') // ✅ Autocomplete for table names
.select('*') // ✅ Typed return value
.eq('id', caseId)
```
**Remember to regenerate types after migrations:**
```bash
npx supabase gen types typescript --linked > src/lib/database.types.ts
```
## Realtime Subscriptions
```typescript
'use client'
import { useEffect } from 'react'
import { createBrowserClient } from '@/lib/supabase/client'
export function RealtimeComponent() {
const supabase = createBrowserClient()
useEffect(() => {
const channel = supabase
.channel('cases-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'cases',
},
(payload) => {
console.log('Change received!', payload)
// Update UI
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
return <div>Listening for changes...</div>
}
```
## Common Mistakes
### ❌ Mistake #1: Not checking for errors
```typescript
// WRONG - No error handling
const { data } = await supabase.from('cases').select('*')
return data // Could be null!
// CORRECT
const { data, error } = await supabase.from('cases').select('*')
if (error) {
console.error('Error:', error)
return { error: error.message }
}
return { data }
```
### ❌ Mistake #2: Using client in server components
```typescript
// WRONG - Don't import client in server components
import { createBrowserClient } from '@/lib/supabase/client'
export default async function Page() {
const supabase = createBrowserClient() // ERROR!
}
// CORRECT - Use server client
import { createServerClient } from '@/lib/supabase/server'
export default async function Page() {
const supabase = await createServerClient()
}
```
### ❌ Mistake #3: Not securing Server Actions
```typescript
// WRONG - No authentication check
'use server'
export async function deleteCase(caseId: string) {
const supabase = await createServerClient()
await supabase.from('cases').delete().eq('id', caseId)
// Anyone can delete any case!
}
// CORRECT - Verify user owns the case
'use server'
import { auth } from '@clerk/nextjs/server'
export async function deleteCase(caseId: string) {
const { userId } = await auth()
if (!userId) {
throw new Error('Unauthorized')
}
const supabase = await createServerClient()
// Verify ownership before deleting
const { data: caseData } = await supabase
.from('cases')
.select('owner_id')
.eq('id', caseId)
.single()
if (caseData?.owner_id !== userId) {
throw new Error('Unauthorized')
}
await supabase.from('cases').delete().eq('id', caseId)
}
```
### ❌ Mistake #4: Not using indexes
```sql
-- WRONG - Frequently queried column without index
CREATE TABLE cases (
id UUID PRIMARY KEY,
owner_id TEXT NOT NULL -- No index!
);
-- CORRECT - Add indexes for frequently queried columns
CREATE TABLE cases (
id UUID PRIMARY KEY,
owner_id TEXT NOT NULL
);
CREATE INDEX idx_cases_owner ON cases(owner_id);
```
### ❌ Mistake #5: Not using migrations
```typescript
// WRONG - Creating tables via SQL in application code
await supabase.sql`CREATE TABLE ...` // Don't do this!
// CORRECT - Use migrations
// Create: supabase/migrations/20251018_create_table.sql
// Then run: npx supabase db push
```
## Performance Tips
1. **Use indexes** for frequently queried columns
2. **Select specific columns** instead of `select('*')`
3. **Use pagination** with `.range()` for large datasets
4. **Batch operations** when possible
5. **Use RPC functions** for complex operations
6. **Cache results** when appropriate
## Migration Checklist
When creating a migration:
- [ ] Use `npx supabase migration new name`
- [ ] Include `IF NOT EXISTS` clauses
- [ ] Add appropriate indexes for frequently queried columns
- [ ] Add table comment indicating security pattern (RLS vs Clerk + Server Actions)
- [ ] Only enable RLS if needed (most tables use Clerk + Server Actions)
- [ ] Add `updated_at` trigger if needed
- [ ] Add column comments for documentation
- [ ] Test locally with `npx supabase db reset`
- [ ] Push to production with `npx supabase db push`
- [ ] **Regenerate TypeScript types:** `npx supabase gen types typescript --linked > src/lib/database.types.ts`

View File

@@ -0,0 +1,446 @@
# Next.js 15 Patterns - CareBridge
## Critical Changes from Next.js 14
Next.js 15 introduces async Request APIs. Several patterns have changed.
## Async Params (CRITICAL)
### In App Router Pages
**ALL dynamic route params must be awaited**
```typescript
// ❌ WRONG - Next.js 14 pattern (will error in 15)
export default function Page({ params }: { params: { id: string } }) {
const { id } = params // ERROR!
}
// ✅ CORRECT - Next.js 15 pattern
type Props = {
params: Promise<{ id: string }>
}
export default async function Page({ params }: Props) {
const { id } = await params
// Use id
}
```
### In API Routes
**ALL dynamic route params must be awaited**
```typescript
// ❌ WRONG
export async function GET(
request: Request,
{ params }: { params: { userId: string } }
) {
const { userId } = params // ERROR!
}
// ✅ CORRECT
export async function GET(
request: Request,
{ params }: { params: Promise<{ userId: string }> }
) {
const { userId } = await params
// Use userId
}
```
## Async SearchParams
**SearchParams are also async in Next.js 15**
```typescript
// ❌ WRONG
export default function Page({
searchParams,
}: {
searchParams: { caseId?: string }
}) {
const caseId = searchParams.caseId // ERROR!
}
// ✅ CORRECT
type SearchParams = Promise<{ caseId?: string }>
export default async function Page({
searchParams,
}: {
searchParams: SearchParams
}) {
const params = await searchParams
const caseId = getCaseIdFromParams(params)
}
```
## Server Components vs Client Components
### When to Use Server Components (Default)
Use server components (no `'use client'`) for:
- Pages that fetch data
- Pages that need SEO
- Static content
- Database queries
```typescript
// Server Component (default)
import { createServerClient } from '@/lib/supabase/server'
export default async function DashboardPage() {
const supabase = await createServerClient()
const { data } = await supabase.from('cases').select('*')
return <div>{/* Render data */}</div>
}
```
### When to Use Client Components
Use `'use client'` for:
- Interactive UI (onClick, onChange, etc.)
- React hooks (useState, useEffect, useContext)
- Browser APIs (localStorage, window, etc.)
- Third-party libraries that need client-side
```typescript
'use client'
import { useState } from 'react'
export function InteractiveComponent() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
```
### Mixing Server and Client
**Pattern**: Server component fetches data, passes to client component
```typescript
// app/page.tsx (Server Component)
import { ClientComponent } from './client-component'
export default async function Page() {
const data = await fetchData()
return <ClientComponent data={data} />
}
// client-component.tsx (Client Component)
'use client'
export function ClientComponent({ data }) {
const [selected, setSelected] = useState(null)
return (
<div onClick={() => setSelected(data)}>
{/* Interactive UI */}
</div>
)
}
```
## Metadata API
Use the Metadata API for SEO:
```typescript
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Dashboard | CareBridge',
description: 'Manage your care cases',
}
export default function Page() {
return <div>Dashboard</div>
}
```
## Loading and Error States
### Loading States
Create `loading.tsx` next to `page.tsx`:
```typescript
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>
}
```
### Error Boundaries
Create `error.tsx` next to `page.tsx`:
```typescript
// app/dashboard/error.tsx
'use client' // Error boundaries must be client components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}
```
## Data Fetching Patterns
### Server-Side Fetching (Preferred)
```typescript
// In Server Component
export default async function Page() {
const supabase = await createServerClient()
const { data } = await supabase.from('cases').select('*')
return <CasesList cases={data} />
}
```
### Client-Side Fetching (When Needed)
```typescript
'use client'
import { useEffect, useState } from 'react'
export function ClientDataFetcher() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then(setData)
}, [])
if (!data) return <div>Loading...</div>
return <div>{/* Render data */}</div>
}
```
## Form Actions
Use Server Actions for forms:
```typescript
// app/actions.ts
'use server'
export async function createCase(formData: FormData) {
const name = formData.get('name')
const supabase = await createServerClient()
await supabase.from('cases').insert({ name })
revalidatePath('/dashboard')
}
// app/page.tsx
import { createCase } from './actions'
export default function Page() {
return (
<form action={createCase}>
<input name="name" />
<button type="submit">Create</button>
</form>
)
}
```
## Common Mistakes
### ❌ Mistake #1: Forgetting 'use client'
```typescript
// WRONG - Will error because useState needs client
import { useState } from 'react'
export function Component() {
const [count, setCount] = useState(0) // ERROR!
}
// CORRECT
'use client'
import { useState } from 'react'
export function Component() {
const [count, setCount] = useState(0)
}
```
### ❌ Mistake #2: Not awaiting params
```typescript
// WRONG
export default function Page({ params }) {
const { id } = params // ERROR in Next.js 15!
}
// CORRECT
export default async function Page({ params }) {
const { id } = await params
}
```
### ❌ Mistake #3: Using client-side hooks in server components
```typescript
// WRONG
export default function Page() {
const router = useRouter() // ERROR!
}
// CORRECT - Add 'use client'
'use client'
export default function Page() {
const router = useRouter()
}
```
### ❌ Mistake #4: Importing server-only code in client components
```typescript
// WRONG - Client component trying to use server-only code
'use client'
import { createServerClient } from '@/lib/supabase/server' // ERROR!
export function Component() {
const supabase = await createServerClient() // Won't work!
}
// CORRECT - Use Server Action instead
'use client'
import { getData } from './actions'
export function Component() {
const handleClick = async () => {
const data = await getData() // Calls server action
}
}
```
## Environment Variables
### Public vs Private
```typescript
// ✅ Public (available in browser)
const key = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
// ✅ Private (server-only)
const secret = process.env.STRIPE_SECRET_KEY // Only works in server components/actions
```
### Accessing in Client Components
```typescript
// ❌ WRONG - Private env vars don't work in client
'use client'
const secret = process.env.STRIPE_SECRET_KEY // undefined!
// ✅ CORRECT - Use public env vars
'use client'
const publicKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
```
## Caching
Next.js 15 has granular caching:
```typescript
// Cache indefinitely (static data)
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache',
})
// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 },
})
// Never cache (dynamic data)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
```
## Route Handlers (API Routes)
```typescript
// app/api/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
return Response.json({ id })
}
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ success: true })
}
```
## Dynamic Routes
```typescript
// app/cases/[caseId]/page.tsx
type Props = {
params: Promise<{ caseId: string }>
}
export default async function CasePage({ params }: Props) {
const { caseId } = await params
return <div>Case: {caseId}</div>
}
```
## Middleware
Use for auth checks, redirects, etc.:
```typescript
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/cases(.*)',
])
export default clerkMiddleware((auth, req) => {
if (isProtectedRoute(req)) auth().protect()
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}
```

View File

@@ -0,0 +1,331 @@
# Stripe Integration - CareBridge
## Overview
CareBridge uses Stripe for:
- **SaaS Subscription**: $19.99/month platform access
- **Concierge Packages**: 4 service packages (Essentials, Benefits, Concierge Plus, White-Glove)
## Architecture
### Pricing Configuration (Single Source of Truth)
**ALL pricing lives in `src/lib/config/concierge-pricing.ts`**
```typescript
export const CONCIERGE_PACKAGES: Record<PackageType, ConciergePackage> = {
essentials: {
name: 'Essentials Package',
priceRange: '$399 $899',
payment_type: 'one_time',
pricing: {
type: 'range', // Allows any price in range
min: 39900, // $399 in cents
max: 89900 // $899 in cents
}
},
concierge_plus: {
name: 'Concierge Plus',
priceRange: '$249 $499/month',
payment_type: 'subscription',
pricing: {
type: 'tiers', // Predefined Stripe price IDs
tiers: [
{
name: 'Light Support',
price: 24900,
priceId: process.env.NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_LIGHT_PRICE_ID,
},
// ... more tiers
]
}
}
}
```
### Flexible Pricing System
CareBridge supports two pricing approaches:
1. **Tiered Pricing** (Concierge Plus, White-Glove)
- Predefined Stripe price IDs
- User selects from available tiers
- Uses Stripe's price objects
2. **Range Pricing** (Essentials, Benefits)
- Custom pricing within a range
- Uses Stripe `price_data` for dynamic pricing
- Staff can set exact price during booking
## Stripe Product Setup
### Automated Setup Script
```bash
cd scripts
./setup-carebridge-pricing.sh
```
This creates:
- SaaS subscription product ($19.99/mo)
- Concierge Plus tiers (3 prices)
- White-Glove tiers (3 prices)
- Test customer
- Generates .env.stripe with all IDs
### Environment Variables
Required in `.env.local`:
```bash
# Stripe Keys
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# SaaS Subscription
NEXT_PUBLIC_STRIPE_SAAS_MONTHLY_PRICE_ID=price_...
# Concierge Plus Tiers
NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_LIGHT_PRICE_ID=price_...
NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_STANDARD_PRICE_ID=price_...
NEXT_PUBLIC_STRIPE_CONCIERGE_PLUS_PREMIUM_PRICE_ID=price_...
# White-Glove Tiers
NEXT_PUBLIC_STRIPE_WHITE_GLOVE_STANDARD_PRICE_ID=price_...
NEXT_PUBLIC_STRIPE_WHITE_GLOVE_PREMIUM_PRICE_ID=price_...
NEXT_PUBLIC_STRIPE_WHITE_GLOVE_ENTERPRISE_PRICE_ID=price_...
```
## Creating Checkout Sessions
### SaaS Subscription
```typescript
// Fixed price - no user input needed
const response = await fetch('/api/create-subscription-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseId }), // Preserve case context
})
```
API route automatically uses the SaaS price ID from env vars.
### Concierge Packages
```typescript
import { createConciergeCheckout } from '@/lib/actions/concierge-actions'
// For tiered pricing (Concierge Plus, White-Glove)
const result = await createConciergeCheckout({
package_type: 'concierge_plus',
price_cents: 24900, // Must match a tier price
display_name: 'Light Support',
case_id: caseId,
})
// For range pricing (Essentials, Benefits)
const result = await createConciergeCheckout({
package_type: 'essentials',
price_cents: 50000, // Any value between min/max
display_name: 'Essentials Package',
case_id: caseId,
})
```
The action automatically:
- Validates price is within allowed range/tiers
- Creates Stripe checkout with correct mode (subscription vs payment)
- Adds metadata for webhook routing
- Returns checkout URL
## Webhook Handling
**File**: `src/app/api/webhooks/stripe/route.ts`
### Metadata-Driven Routing
Webhooks determine the type by checking `session.metadata.package_type`:
```typescript
case 'checkout.session.completed': {
if (session.metadata?.package_type) {
// Concierge package purchase
await handleConciergeCheckout(session)
} else if (session.subscription) {
// SaaS subscription
await handleSaaSSubscriptionCheckout(session)
}
break
}
```
### Required Metadata
Always include in checkout sessions:
```typescript
metadata: {
clerk_user_id: userId,
package_type: 'concierge_plus', // For concierge packages
payment_type: 'subscription', // one_time, project_based, or subscription
price_display_name: 'Light Support',
}
```
### Webhook Events
Handle these events:
- `checkout.session.completed` - Create subscription/package record
- `customer.subscription.updated` - Update subscription status
- `customer.subscription.deleted` - Mark as cancelled
## Database Structure
### SaaS Subscriptions
Table: `subscriptions`
```sql
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
clerk_user_id TEXT NOT NULL,
stripe_subscription_id VARCHAR(255),
is_active BOOLEAN DEFAULT false, -- Simple boolean, no tiers
status VARCHAR(50),
current_period_end TIMESTAMPTZ,
-- ...
)
```
### Concierge Packages
Table: `concierge_packages`
```sql
CREATE TABLE concierge_packages (
id UUID PRIMARY KEY,
clerk_user_id TEXT NOT NULL,
package_type VARCHAR(50) NOT NULL, -- essentials, benefits, etc.
payment_type VARCHAR(20) NOT NULL, -- one_time, project_based, subscription
stripe_payment_intent_id VARCHAR(255), -- For one-time
stripe_subscription_id VARCHAR(255), -- For subscriptions
price_paid_cents INT NOT NULL,
price_display_name VARCHAR(100),
status VARCHAR(20) DEFAULT 'pending',
-- ...
)
```
## Testing
### Local Webhook Testing
```bash
# Terminal 1: Start webhook listener
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Copy the webhook secret (whsec_...) to .env.local as STRIPE_WEBHOOK_SECRET
# Terminal 2: Start dev server
npm run dev
```
### Test Cards
- Success: `4242 4242 4242 4242`
- Requires Auth: `4000 0025 0000 3155`
- Declined: `4000 0000 0000 9995`
Expiry: Any future date | CVC: Any 3 digits | ZIP: Any 5 digits
### Testing Checklist
1. ✅ SaaS subscription checkout
2. ✅ Concierge package with tiers
3. ✅ Concierge package with custom price
4. ✅ Webhook creates database records
5. ✅ Success page displays correctly
6. ✅ Billing page shows all subscriptions
## Common Mistakes
### ❌ Mistake #1: Not validating price
```typescript
// WRONG - No validation
await createConciergeCheckout({
price_cents: 1000000, // Way above max!
})
// CORRECT - Use the action, it validates automatically
const result = await createConciergeCheckout({
package_type: 'essentials',
price_cents: 50000,
display_name: 'Essentials',
})
if (!result.success) {
// Handle validation error
}
```
### ❌ Mistake #2: Hardcoding price IDs
```typescript
// WRONG - Hardcoded
const priceId = 'price_abc123'
// CORRECT - Use env vars
const priceId = process.env.NEXT_PUBLIC_STRIPE_SAAS_MONTHLY_PRICE_ID
```
### ❌ Mistake #3: Not including metadata
```typescript
// WRONG - Webhook won't know how to route
const session = await stripe.checkout.sessions.create({
line_items: [...],
mode: 'subscription',
})
// CORRECT - Include routing metadata
const session = await stripe.checkout.sessions.create({
line_items: [...],
mode: 'subscription',
metadata: {
clerk_user_id: userId,
package_type: 'concierge_plus',
payment_type: 'subscription',
},
})
```
### ❌ Mistake #4: Forgetting to await Stripe responses
```typescript
// WRONG - Missing await
const session = stripe.checkout.sessions.create({...})
// CORRECT
const session = await stripe.checkout.sessions.create({...})
```
## Changing Pricing
To change pricing (add tiers, adjust prices, etc.):
1. Update `src/lib/config/concierge-pricing.ts`
2. Create new Stripe products/prices
3. Update environment variables
4. No backend code changes needed!
The flexible architecture supports any pricing changes without touching server actions or webhooks.
## Documentation
- `STRIPE-SETUP-GUIDE.md` - Complete setup instructions
- `CAREBRIDGE-PRICING-IMPLEMENTATION.md` - Technical deep dive
- `scripts/README.md` - Script documentation