516 lines
13 KiB
Markdown
516 lines
13 KiB
Markdown
# Example: Personal Task Manager with Authentication
|
|
|
|
This example shows planning docs for a task manager with user authentication using Clerk.
|
|
|
|
**User Request**: "I want to build a task manager where users can sign up, log in, and manage their own tasks privately. Users should be able to organize tasks with tags and due dates."
|
|
|
|
---
|
|
|
|
## IMPLEMENTATION_PHASES.md (Excerpt)
|
|
|
|
# Implementation Phases: Personal Task Manager
|
|
|
|
**Project Type**: Authenticated Web App (Multi-user CRUD)
|
|
**Stack**: Cloudflare Workers + Vite + React + Tailwind v4 + shadcn/ui + D1 + Clerk
|
|
**Estimated Total**: 20 hours (~20 minutes human time)
|
|
**Created**: 2025-10-25
|
|
|
|
---
|
|
|
|
## Phase 1: Project Setup
|
|
**Type**: Infrastructure
|
|
**Estimated**: 2-3 hours
|
|
**Files**: `package.json`, `wrangler.jsonc`, `vite.config.ts`, `src/index.ts`
|
|
|
|
[Same structure as simple example]
|
|
|
|
---
|
|
|
|
## Phase 2: Database Schema
|
|
**Type**: Database
|
|
**Estimated**: 3-4 hours
|
|
**Files**: `migrations/0001_initial.sql`, `src/lib/db-types.ts`
|
|
|
|
### Tasks
|
|
- [ ] Create D1 database
|
|
- [ ] Design schema for users, tasks, tags, task_tags tables
|
|
- [ ] Write migration SQL
|
|
- [ ] Create TypeScript types
|
|
- [ ] Apply migration locally
|
|
|
|
### Verification Criteria
|
|
- [ ] All tables created successfully
|
|
- [ ] Foreign keys work (task references user)
|
|
- [ ] Unique constraints work (user email, tag name per user)
|
|
- [ ] Can query with joins (tasks with their tags)
|
|
|
|
### Exit Criteria
|
|
Complete database schema with relationships working.
|
|
|
|
---
|
|
|
|
## Phase 3: Clerk Authentication Setup
|
|
**Type**: Integration
|
|
**Estimated**: 3 hours
|
|
**Files**: `src/main.tsx`, `src/middleware/auth.ts`, `src/lib/clerk-types.ts`
|
|
|
|
### Tasks
|
|
- [ ] Create Clerk account and application
|
|
- [ ] Install `@clerk/clerk-react` and `@clerk/backend`
|
|
- [ ] Configure Clerk in frontend (ClerkProvider)
|
|
- [ ] Create custom JWT template in Clerk dashboard
|
|
- [ ] Implement auth middleware for Worker
|
|
- [ ] Test JWT verification
|
|
- [ ] Create protected route wrapper component
|
|
|
|
### Verification Criteria
|
|
- [ ] Can sign up new user (Clerk modal)
|
|
- [ ] Can sign in existing user
|
|
- [ ] JWT is included in API requests
|
|
- [ ] Worker middleware verifies JWT correctly
|
|
- [ ] Invalid JWT returns 401
|
|
- [ ] Protected routes redirect if not authenticated
|
|
|
|
### Exit Criteria
|
|
Authentication flow working end-to-end. Users can sign up, log in, and access protected routes.
|
|
|
|
---
|
|
|
|
## Phase 4: User Sync (Webhook)
|
|
**Type**: Integration
|
|
**Estimated**: 2 hours
|
|
**Files**: `src/routes/webhooks.ts`, `src/lib/webhook-verify.ts`
|
|
|
|
### Tasks
|
|
- [ ] Install `svix` package
|
|
- [ ] Create webhook endpoint `/api/webhooks/clerk`
|
|
- [ ] Implement signature verification
|
|
- [ ] Handle `user.created` event (insert into database)
|
|
- [ ] Handle `user.updated` event
|
|
- [ ] Handle `user.deleted` event
|
|
- [ ] Configure webhook in Clerk dashboard
|
|
|
|
### Verification Criteria
|
|
- [ ] New user signup triggers webhook
|
|
- [ ] User record created in D1 database
|
|
- [ ] Invalid webhook signature rejected
|
|
- [ ] User updates sync to database
|
|
- [ ] User deletions remove tasks (cascade)
|
|
|
|
### Exit Criteria
|
|
User data synced between Clerk and D1 database.
|
|
|
|
---
|
|
|
|
## Phase 5: Tasks API
|
|
**Type**: API
|
|
**Estimated**: 5 hours
|
|
**Files**: `src/routes/tasks.ts`, `src/lib/schemas.ts`
|
|
|
|
### Tasks
|
|
- [ ] Define Zod schemas for task validation
|
|
- [ ] GET /api/tasks (user's tasks only)
|
|
- [ ] POST /api/tasks (create for current user)
|
|
- [ ] PATCH /api/tasks/:id (update if user owns)
|
|
- [ ] DELETE /api/tasks/:id (delete if user owns)
|
|
- [ ] Add authorization checks (verify ownership)
|
|
- [ ] Filter by completion, tag, due date
|
|
|
|
### Verification Criteria
|
|
- [ ] GET /api/tasks returns only current user's tasks
|
|
- [ ] Cannot access other users' tasks
|
|
- [ ] Cannot update/delete other users' tasks (403)
|
|
- [ ] Filters work (completed, tag, due date)
|
|
- [ ] All CRUD operations tested
|
|
|
|
### Exit Criteria
|
|
Tasks API complete with proper authorization.
|
|
|
|
---
|
|
|
|
## Phase 6: Tags API
|
|
**Type**: API
|
|
**Estimated**: 2 hours
|
|
**Files**: `src/routes/tags.ts`
|
|
|
|
### Tasks
|
|
- [ ] GET /api/tags (user's tags)
|
|
- [ ] POST /api/tags (create tag)
|
|
- [ ] DELETE /api/tags/:id (delete tag)
|
|
- [ ] POST /api/tasks/:id/tags (add tag to task)
|
|
- [ ] DELETE /api/tasks/:id/tags/:tagId (remove tag from task)
|
|
|
|
### Verification Criteria
|
|
- [ ] Can create tags
|
|
- [ ] Can attach tags to tasks
|
|
- [ ] Can filter tasks by tag
|
|
- [ ] Deleting tag doesn't delete tasks
|
|
|
|
### Exit Criteria
|
|
Tag management working with task associations.
|
|
|
|
---
|
|
|
|
## Phase 7: Dashboard UI
|
|
**Type**: UI
|
|
**Estimated**: 6 hours
|
|
**Files**: `src/pages/Dashboard.tsx`, `src/components/layout/Sidebar.tsx`, etc.
|
|
|
|
### Tasks
|
|
- [ ] Build dashboard layout with sidebar
|
|
- [ ] Add user menu with sign out
|
|
- [ ] Create task list view
|
|
- [ ] Add task filtering (completed, tags, due date)
|
|
- [ ] Build task creation dialog
|
|
- [ ] Build task editing dialog
|
|
- [ ] Add task deletion with confirmation
|
|
|
|
### Verification Criteria
|
|
- [ ] Dashboard shows user's tasks
|
|
- [ ] Filters work correctly
|
|
- [ ] Can create tasks via dialog
|
|
- [ ] Can edit existing tasks
|
|
- [ ] Can delete tasks
|
|
- [ ] UI updates optimistically
|
|
|
|
### Exit Criteria
|
|
Complete dashboard with full task management.
|
|
|
|
---
|
|
|
|
## Phase 8: Tags UI
|
|
**Type**: UI
|
|
**Estimated**: 3 hours
|
|
**Files**: `src/components/TagManager.tsx`, `src/components/TagBadge.tsx`
|
|
|
|
### Tasks
|
|
- [ ] Build tag creation form
|
|
- [ ] Build tag list component
|
|
- [ ] Add tag selection to task form (multi-select)
|
|
- [ ] Display tags as badges on tasks
|
|
- [ ] Allow removing tags from tasks
|
|
|
|
### Verification Criteria
|
|
- [ ] Can create new tags
|
|
- [ ] Can select tags when creating/editing task
|
|
- [ ] Tags display on task cards
|
|
- [ ] Can filter by tag
|
|
- [ ] Can remove tags from tasks
|
|
|
|
### Exit Criteria
|
|
Full tag management integrated with tasks.
|
|
|
|
---
|
|
|
|
## DATABASE_SCHEMA.md (Excerpt)
|
|
|
|
# Database Schema: Personal Task Manager
|
|
|
|
---
|
|
|
|
## Tables
|
|
|
|
### `users`
|
|
| Column | Type | Constraints | Notes |
|
|
|--------|------|-------------|-------|
|
|
| `id` | INTEGER | PRIMARY KEY | Auto-increment |
|
|
| `clerk_id` | TEXT | UNIQUE, NOT NULL | Clerk user ID |
|
|
| `email` | TEXT | UNIQUE, NOT NULL | User email |
|
|
| `display_name` | TEXT | NULL | Display name |
|
|
| `created_at` | INTEGER | NOT NULL | Unix timestamp |
|
|
| `updated_at` | INTEGER | NOT NULL | Unix timestamp |
|
|
|
|
**Indexes**: `idx_users_clerk_id`, `idx_users_email`
|
|
|
|
---
|
|
|
|
### `tasks`
|
|
| Column | Type | Constraints | Notes |
|
|
|--------|------|-------------|-------|
|
|
| `id` | INTEGER | PRIMARY KEY | Auto-increment |
|
|
| `user_id` | INTEGER | FOREIGN KEY, NOT NULL | References users(id) |
|
|
| `title` | TEXT | NOT NULL | Task title |
|
|
| `description` | TEXT | NULL | Task description |
|
|
| `completed` | INTEGER | NOT NULL | 0 or 1 |
|
|
| `due_date` | INTEGER | NULL | Unix timestamp |
|
|
| `created_at` | INTEGER | NOT NULL | Unix timestamp |
|
|
| `updated_at` | INTEGER | NOT NULL | Unix timestamp |
|
|
|
|
**Indexes**: `idx_tasks_user_id`, `idx_tasks_due_date`, `idx_tasks_completed`
|
|
|
|
**Relationships**: Many-to-one with users
|
|
|
|
**Cascade**: ON DELETE CASCADE (deleting user deletes their tasks)
|
|
|
|
---
|
|
|
|
### `tags`
|
|
| Column | Type | Constraints | Notes |
|
|
|--------|------|-------------|-------|
|
|
| `id` | INTEGER | PRIMARY KEY | Auto-increment |
|
|
| `user_id` | INTEGER | FOREIGN KEY, NOT NULL | References users(id) |
|
|
| `name` | TEXT | NOT NULL | Tag name |
|
|
| `color` | TEXT | NOT NULL | Hex color code |
|
|
| `created_at` | INTEGER | NOT NULL | Unix timestamp |
|
|
|
|
**Indexes**: `idx_tags_user_id`, `idx_tags_name_user_id UNIQUE`
|
|
|
|
**Unique Constraint**: (user_id, name) - users can't have duplicate tag names
|
|
|
|
---
|
|
|
|
### `task_tags`
|
|
| Column | Type | Constraints | Notes |
|
|
|--------|------|-------------|-------|
|
|
| `id` | INTEGER | PRIMARY KEY | Auto-increment |
|
|
| `task_id` | INTEGER | FOREIGN KEY, NOT NULL | References tasks(id) |
|
|
| `tag_id` | INTEGER | FOREIGN KEY, NOT NULL | References tags(id) |
|
|
| `created_at` | INTEGER | NOT NULL | Unix timestamp |
|
|
|
|
**Indexes**: `idx_task_tags_task_id`, `idx_task_tags_tag_id`, `idx_task_tags_composite UNIQUE`
|
|
|
|
**Unique Constraint**: (task_id, tag_id) - can't add same tag to task twice
|
|
|
|
**Cascade**: ON DELETE CASCADE (deleting task or tag removes association)
|
|
|
|
---
|
|
|
|
## Relationships Diagram
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ users │
|
|
└──────┬──────┘
|
|
│ 1
|
|
│
|
|
├─────────────────────┬
|
|
│ N │ N
|
|
┌──────┴──────────┐ ┌──────┴──────────┐
|
|
│ tasks │ │ tags │
|
|
└──────┬──────────┘ └──────┬──────────┘
|
|
│ N │ N
|
|
│ │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────┴──────────┐
|
|
│ task_tags │
|
|
│ (junction) │
|
|
└─────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## API_ENDPOINTS.md (Excerpt)
|
|
|
|
# API Endpoints: Personal Task Manager
|
|
|
|
**Auth**: Required on all `/api/*` routes (except webhooks)
|
|
|
|
---
|
|
|
|
## Authentication
|
|
|
|
### GET `/api/auth/me`
|
|
**Purpose**: Get current user profile
|
|
|
|
**Response 200**:
|
|
```json
|
|
{
|
|
"data": {
|
|
"id": 1,
|
|
"email": "user@example.com",
|
|
"displayName": "John Doe",
|
|
"createdAt": 1234567890
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tasks
|
|
|
|
### GET `/api/tasks`
|
|
**Purpose**: Get current user's tasks
|
|
|
|
**Query Parameters**:
|
|
- `completed` (optional): `true` or `false`
|
|
- `tag` (optional): Tag ID to filter by
|
|
- `dueBefore` (optional): Unix timestamp
|
|
- `dueAfter` (optional): Unix timestamp
|
|
|
|
**Response 200**:
|
|
```json
|
|
{
|
|
"data": [
|
|
{
|
|
"id": 1,
|
|
"title": "Review PR",
|
|
"description": "Check the new feature",
|
|
"completed": false,
|
|
"dueDate": 1234567890,
|
|
"tags": [
|
|
{ "id": 1, "name": "work", "color": "#3b82f6" }
|
|
],
|
|
"createdAt": 1234567890,
|
|
"updatedAt": 1234567890
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST `/api/tasks`
|
|
**Purpose**: Create task for current user
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"title": "New Task",
|
|
"description": "Optional",
|
|
"dueDate": 1234567890,
|
|
"tagIds": [1, 2]
|
|
}
|
|
```
|
|
|
|
**Validation**:
|
|
```typescript
|
|
z.object({
|
|
title: z.string().min(1).max(100),
|
|
description: z.string().optional(),
|
|
dueDate: z.number().optional(),
|
|
tagIds: z.array(z.number()).optional()
|
|
})
|
|
```
|
|
|
|
**Response 201**: Created task with tags
|
|
|
|
---
|
|
|
|
## Tags
|
|
|
|
### GET `/api/tags`
|
|
**Purpose**: Get current user's tags
|
|
|
|
**Response 200**:
|
|
```json
|
|
{
|
|
"data": [
|
|
{ "id": 1, "name": "work", "color": "#3b82f6" },
|
|
{ "id": 2, "name": "personal", "color": "#10b981" }
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST `/api/tags`
|
|
**Purpose**: Create tag for current user
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"name": "urgent",
|
|
"color": "#ef4444"
|
|
}
|
|
```
|
|
|
|
**Response 201**: Created tag
|
|
|
|
**Response 400**: Duplicate tag name (user already has tag with this name)
|
|
|
|
---
|
|
|
|
## INTEGRATION.md (Excerpt)
|
|
|
|
# Third-Party Integrations: Personal Task Manager
|
|
|
|
---
|
|
|
|
## Clerk (Authentication)
|
|
|
|
**Purpose**: User authentication and management
|
|
|
|
**Environment Variables**:
|
|
```env
|
|
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
|
|
CLERK_SECRET_KEY=sk_test_...
|
|
CLERK_WEBHOOK_SECRET=whsec_...
|
|
```
|
|
|
|
**Custom JWT Template** (Clerk dashboard):
|
|
```json
|
|
{
|
|
"email": "{{user.primary_email_address}}",
|
|
"userId": "{{user.id}}",
|
|
"firstName": "{{user.first_name}}",
|
|
"lastName": "{{user.last_name}}"
|
|
}
|
|
```
|
|
|
|
**Frontend**:
|
|
```tsx
|
|
import { ClerkProvider, SignedIn, SignedOut, UserButton } from '@clerk/clerk-react'
|
|
|
|
<ClerkProvider publishableKey={...}>
|
|
<SignedIn>
|
|
<Dashboard />
|
|
<UserButton />
|
|
</SignedIn>
|
|
<SignedOut>
|
|
<LandingPage />
|
|
</SignedOut>
|
|
</ClerkProvider>
|
|
```
|
|
|
|
**Backend Middleware**:
|
|
```typescript
|
|
import { verifyToken } from '@clerk/backend'
|
|
|
|
export async function authMiddleware(c: Context, next: Next) {
|
|
const token = c.req.header('Authorization')?.replace('Bearer ', '')
|
|
const verified = await verifyToken(token, {
|
|
secretKey: c.env.CLERK_SECRET_KEY
|
|
})
|
|
c.set('clerkUserId', verified.userId)
|
|
c.set('email', verified.email)
|
|
|
|
// Get local user ID from database
|
|
const user = await c.env.DB.prepare(
|
|
'SELECT id FROM users WHERE clerk_id = ?'
|
|
).bind(verified.userId).first()
|
|
|
|
c.set('userId', user.id)
|
|
await next()
|
|
}
|
|
```
|
|
|
|
**Webhook** (User Sync):
|
|
```typescript
|
|
app.post('/api/webhooks/clerk', async (c) => {
|
|
const payload = await c.req.text()
|
|
const verified = await verifyClerkWebhook(payload, c.req.raw.headers, c.env.CLERK_WEBHOOK_SECRET)
|
|
|
|
const event = JSON.parse(payload)
|
|
|
|
if (event.type === 'user.created') {
|
|
await c.env.DB.prepare(`
|
|
INSERT INTO users (clerk_id, email, display_name, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).bind(
|
|
event.data.id,
|
|
event.data.email_addresses[0].email_address,
|
|
`${event.data.first_name} ${event.data.last_name}`,
|
|
Date.now(),
|
|
Date.now()
|
|
).run()
|
|
}
|
|
|
|
return c.json({ received: true })
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
**Note**: This example shows the complete structure for an authenticated multi-user app. For AI-powered features, see `ai-web-app.md`.
|