Initial commit
This commit is contained in:
11
.claude-plugin/plugin.json
Normal file
11
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
288
SKILL.md
Normal 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
65
plugin.lock.json
Normal 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
189
references/case-context.md
Normal 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.
|
||||||
435
references/component-standards.md
Normal file
435
references/component-standards.md
Normal 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?
|
||||||
588
references/database-patterns.md
Normal file
588
references/database-patterns.md
Normal 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`
|
||||||
446
references/nextjs-15-patterns.md
Normal file
446
references/nextjs-15-patterns.md
Normal 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)(.*)'],
|
||||||
|
}
|
||||||
|
```
|
||||||
331
references/stripe-integration.md
Normal file
331
references/stripe-integration.md
Normal 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
|
||||||
Reference in New Issue
Block a user