create-baton 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/constants.js +3 -3
- package/templates/BATON_v3.1.md +53 -15
- package/templates/skills/domains/api/SKILL.md +318 -0
- package/templates/skills/domains/ecommerce/SKILL.md +277 -0
- package/templates/skills/domains/portfolio/SKILL.md +213 -0
- package/templates/skills/domains/saas/SKILL.md +252 -0
- package/templates/skills/patterns/authentication/SKILL.md +245 -0
- package/templates/skills/patterns/database-design/SKILL.md +230 -0
- package/templates/skills/patterns/email/SKILL.md +236 -0
- package/templates/skills/patterns/file-uploads/SKILL.md +216 -0
- package/templates/skills/patterns/payments/SKILL.md +246 -0
- package/templates/skills/patterns/seo/SKILL.md +219 -0
- package/templates/skills/stacks/prisma/SKILL.md +281 -0
- package/templates/skills/stacks/shadcn/SKILL.md +270 -0
- package/templates/skills/stacks/tailwind/SKILL.md +242 -0
- package/templates/skills/stacks/typescript/SKILL.md +241 -0
- package/templates/skills/stacks/vercel/SKILL.md +232 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: saas
|
|
3
|
+
description: >-
|
|
4
|
+
SaaS product patterns — multi-tenancy, workspaces, billing, onboarding,
|
|
5
|
+
feature gating, and usage tracking. Load at Session 0 when building a
|
|
6
|
+
SaaS product. Stays loaded for the entire project lifecycle.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# SaaS Domain Skill
|
|
10
|
+
|
|
11
|
+
> SaaS is not just an app with login. It's an app with organizations, billing, and self-service.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Load This Skill When
|
|
16
|
+
|
|
17
|
+
- User says they're building a SaaS, platform, or subscription product
|
|
18
|
+
- Project has multiple users, teams, or organizations
|
|
19
|
+
- Revenue model is recurring (monthly/annual subscriptions)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture Decisions (Session 0-1)
|
|
24
|
+
|
|
25
|
+
### Multi-Tenancy Model
|
|
26
|
+
|
|
27
|
+
Choose ONE. Don't change mid-project.
|
|
28
|
+
|
|
29
|
+
| Model | How It Works | Best For |
|
|
30
|
+
|-------|-------------|----------|
|
|
31
|
+
| **Shared database, tenant column** | All tenants in same tables, filtered by `org_id` | Most SaaS (start here) |
|
|
32
|
+
| **Schema per tenant** | Separate database schema per tenant | Regulated industries |
|
|
33
|
+
| **Database per tenant** | Completely isolated databases | Enterprise, compliance-heavy |
|
|
34
|
+
|
|
35
|
+
**Default:** Shared database with tenant column. It's simplest and scales to 10,000+ tenants.
|
|
36
|
+
|
|
37
|
+
```sql
|
|
38
|
+
-- Every table gets an org_id
|
|
39
|
+
CREATE TABLE projects (
|
|
40
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
41
|
+
org_id UUID NOT NULL REFERENCES organizations(id),
|
|
42
|
+
name TEXT NOT NULL,
|
|
43
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
-- RLS enforces tenant isolation
|
|
47
|
+
CREATE POLICY "Tenant isolation"
|
|
48
|
+
ON projects FOR ALL
|
|
49
|
+
USING (org_id = (SELECT current_org_id()));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Core Data Model
|
|
53
|
+
|
|
54
|
+
Every SaaS needs these tables minimum:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
users — Individual people
|
|
58
|
+
organizations — Workspaces/teams (the billing entity)
|
|
59
|
+
memberships — users ↔ organizations (with role)
|
|
60
|
+
subscriptions — org ↔ plan (billing state)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Don't build more until you need it.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Onboarding Flow (Session 2-3)
|
|
68
|
+
|
|
69
|
+
### First-Time User Experience
|
|
70
|
+
|
|
71
|
+
The first 60 seconds determine if a user stays. Design for this:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
1. Sign up (email + password, or OAuth)
|
|
75
|
+
2. Create or join organization
|
|
76
|
+
3. ONE guided action (create first [thing])
|
|
77
|
+
4. Success state — "You're set up!"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Rules:**
|
|
81
|
+
- Maximum 3 steps before user sees value
|
|
82
|
+
- Pre-fill defaults where possible
|
|
83
|
+
- Skip optional steps — let users configure later
|
|
84
|
+
- Show progress indicator if more than 2 steps
|
|
85
|
+
|
|
86
|
+
### Empty States
|
|
87
|
+
|
|
88
|
+
Every list/dashboard screen needs an empty state with:
|
|
89
|
+
- What this screen is for (one sentence)
|
|
90
|
+
- A call to action (button to create first item)
|
|
91
|
+
- Optional: example data or template
|
|
92
|
+
|
|
93
|
+
Never show an empty table with column headers and no rows.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Billing Integration (Session 3-5)
|
|
98
|
+
|
|
99
|
+
### Stripe (Default Choice)
|
|
100
|
+
|
|
101
|
+
Unless user specifies otherwise, use Stripe. It handles 90% of SaaS billing needs.
|
|
102
|
+
|
|
103
|
+
**Core flow:**
|
|
104
|
+
```
|
|
105
|
+
User clicks "Upgrade" →
|
|
106
|
+
Redirect to Stripe Checkout →
|
|
107
|
+
Stripe handles payment →
|
|
108
|
+
Webhook confirms payment →
|
|
109
|
+
App updates subscription status
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### What to Build
|
|
113
|
+
|
|
114
|
+
| Component | When | Priority |
|
|
115
|
+
|-----------|------|----------|
|
|
116
|
+
| Pricing page | Session 3-4 | Must have |
|
|
117
|
+
| Stripe Checkout redirect | Session 3-4 | Must have |
|
|
118
|
+
| Webhook handler | Session 3-4 | Must have |
|
|
119
|
+
| Customer portal link | Session 4-5 | Should have |
|
|
120
|
+
| Usage tracking | Session 5+ | Nice to have |
|
|
121
|
+
|
|
122
|
+
### What NOT to Build
|
|
123
|
+
|
|
124
|
+
- Custom payment forms (use Stripe Checkout)
|
|
125
|
+
- Invoice generation (Stripe does this)
|
|
126
|
+
- Tax calculation (Stripe Tax or let Stripe handle it)
|
|
127
|
+
- Subscription management UI (link to Stripe Customer Portal)
|
|
128
|
+
|
|
129
|
+
### Webhook Security
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// ALWAYS verify webhook signatures
|
|
133
|
+
const event = stripe.webhooks.constructEvent(
|
|
134
|
+
body,
|
|
135
|
+
signature,
|
|
136
|
+
process.env.STRIPE_WEBHOOK_SECRET
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Handle these events minimum:
|
|
140
|
+
// checkout.session.completed — new subscription
|
|
141
|
+
// customer.subscription.updated — plan change
|
|
142
|
+
// customer.subscription.deleted — cancellation
|
|
143
|
+
// invoice.payment_failed — payment issue
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Feature Gating (Session 4-5)
|
|
149
|
+
|
|
150
|
+
### Simple Tier Check
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// lib/billing.ts
|
|
154
|
+
export function canAccess(org: Organization, feature: string): boolean {
|
|
155
|
+
const featureMap: Record<string, string[]> = {
|
|
156
|
+
free: ['basic_feature'],
|
|
157
|
+
pro: ['basic_feature', 'advanced_feature', 'api_access'],
|
|
158
|
+
enterprise: ['basic_feature', 'advanced_feature', 'api_access', 'sso', 'audit_log'],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return featureMap[org.plan]?.includes(feature) ?? false;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Rules:**
|
|
166
|
+
- Check features, not plan names (plans change, features don't)
|
|
167
|
+
- Gate at the server, not the client (client gates are cosmetic, not security)
|
|
168
|
+
- Show locked features with upgrade prompt, don't hide them
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Usage Tracking (Session 5+)
|
|
173
|
+
|
|
174
|
+
If billing is usage-based or has limits:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Track usage per org per period
|
|
178
|
+
interface UsageRecord {
|
|
179
|
+
org_id: string;
|
|
180
|
+
metric: string; // 'api_calls' | 'storage_mb' | 'team_members'
|
|
181
|
+
value: number;
|
|
182
|
+
period: string; // '2026-02' (monthly)
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Show usage in dashboard:
|
|
187
|
+
- Current usage vs limit
|
|
188
|
+
- Percentage bar
|
|
189
|
+
- Warning at 80%, block at 100%
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Common SaaS Patterns
|
|
194
|
+
|
|
195
|
+
### Invitation System
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
Owner invites by email →
|
|
199
|
+
Email sent with invite link →
|
|
200
|
+
Invitee signs up or logs in →
|
|
201
|
+
Auto-joined to organization
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Store pending invites. Don't require account creation before accepting.
|
|
205
|
+
|
|
206
|
+
### Role-Based Access
|
|
207
|
+
|
|
208
|
+
Keep it simple. Three roles cover 95% of SaaS:
|
|
209
|
+
|
|
210
|
+
| Role | Can Do |
|
|
211
|
+
|------|--------|
|
|
212
|
+
| **Owner** | Everything + billing + delete org |
|
|
213
|
+
| **Admin** | Everything except billing and delete |
|
|
214
|
+
| **Member** | CRUD own resources, view shared resources |
|
|
215
|
+
|
|
216
|
+
Don't add roles until users ask for them.
|
|
217
|
+
|
|
218
|
+
### Settings Structure
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Settings/
|
|
222
|
+
├── Profile (user-level)
|
|
223
|
+
├── Organization (org-level)
|
|
224
|
+
├── Billing (owner-only)
|
|
225
|
+
├── Team Members (admin+)
|
|
226
|
+
└── Integrations (admin+)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Session Checkpoints
|
|
232
|
+
|
|
233
|
+
### Session 3: Foundation Check
|
|
234
|
+
- [ ] Users can sign up and create organization
|
|
235
|
+
- [ ] Basic CRUD works with tenant isolation
|
|
236
|
+
- [ ] Empty states on all screens
|
|
237
|
+
|
|
238
|
+
### Session 5: Billing Check
|
|
239
|
+
- [ ] Pricing page exists
|
|
240
|
+
- [ ] Stripe integration works (test mode)
|
|
241
|
+
- [ ] Webhook handles subscription events
|
|
242
|
+
- [ ] Feature gating works for at least 2 tiers
|
|
243
|
+
|
|
244
|
+
### Session 8: Pre-Launch Check
|
|
245
|
+
- [ ] Onboarding flow is smooth (test with fresh account)
|
|
246
|
+
- [ ] Billing works in live mode
|
|
247
|
+
- [ ] Invitation system works
|
|
248
|
+
- [ ] Settings pages exist
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
*Last updated: Baton Protocol v3.1*
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authentication
|
|
3
|
+
description: >-
|
|
4
|
+
Authentication patterns — login, signup, OAuth, protected routes, middleware,
|
|
5
|
+
password reset, and session management. Load at Session 1-3 when the project
|
|
6
|
+
needs user accounts. Use when discussing auth strategy or implementing login.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Authentication Skill
|
|
10
|
+
|
|
11
|
+
> Auth is the one thing you can't get wrong. A security hole here exposes everything.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Choose Your Auth Strategy (Session 1)
|
|
16
|
+
|
|
17
|
+
### Decision Tree
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Using Supabase? → Use Supabase Auth (built-in, free)
|
|
21
|
+
Using another database? → Use Better Auth or NextAuth
|
|
22
|
+
Building an API? → Use API keys (see api domain skill)
|
|
23
|
+
Just need social login? → Use OAuth provider directly
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Don't mix strategies. Pick one and commit.
|
|
27
|
+
|
|
28
|
+
### Strategy Comparison
|
|
29
|
+
|
|
30
|
+
| Strategy | Best For | Complexity |
|
|
31
|
+
|----------|----------|-----------|
|
|
32
|
+
| **Supabase Auth** | Next.js + Supabase stack | Low |
|
|
33
|
+
| **Better Auth** | Any stack, full control | Medium |
|
|
34
|
+
| **NextAuth/Auth.js** | Next.js, many providers | Medium |
|
|
35
|
+
| **Custom JWT** | APIs, microservices | High (avoid unless needed) |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Core Auth Flows
|
|
40
|
+
|
|
41
|
+
### Sign Up
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Email + Password → Validate → Create account → Send verification email → Redirect to app
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Rules:**
|
|
48
|
+
- Minimum password length: 8 characters
|
|
49
|
+
- Don't enforce special characters (users hate it, doesn't help security)
|
|
50
|
+
- Email verification is optional for MVP, required for production
|
|
51
|
+
- After signup: redirect to app, not login page
|
|
52
|
+
|
|
53
|
+
### Log In
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Email + Password → Validate → Create session → Redirect to app
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Rules:**
|
|
60
|
+
- Generic error messages: "Invalid email or password" (never reveal which is wrong)
|
|
61
|
+
- Rate limit login attempts (5 per minute per IP)
|
|
62
|
+
- Redirect to intended page after login, not always homepage
|
|
63
|
+
|
|
64
|
+
### Log Out
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Click logout → Destroy session → Redirect to landing page
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Rules:**
|
|
71
|
+
- Clear all session data (cookies, localStorage)
|
|
72
|
+
- Invalidate server-side session
|
|
73
|
+
- Don't ask "are you sure?" — just log out
|
|
74
|
+
|
|
75
|
+
### Password Reset
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Enter email → Send reset link → Click link → Set new password → Auto log in
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Rules:**
|
|
82
|
+
- Reset tokens expire in 1 hour
|
|
83
|
+
- One-time use (invalidate after use)
|
|
84
|
+
- Don't confirm if email exists ("If an account exists, we sent a reset link")
|
|
85
|
+
- After reset: log user in automatically
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Protected Routes
|
|
90
|
+
|
|
91
|
+
### Server-Side (Recommended)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// middleware.ts (Next.js)
|
|
95
|
+
export function middleware(request: NextRequest) {
|
|
96
|
+
const session = request.cookies.get('session');
|
|
97
|
+
|
|
98
|
+
if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
|
|
99
|
+
return NextResponse.redirect(new URL('/login', request.url));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const config = {
|
|
104
|
+
matcher: ['/dashboard/:path*', '/settings/:path*'],
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### In Server Components
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// app/dashboard/page.tsx
|
|
112
|
+
export default async function Dashboard() {
|
|
113
|
+
const user = await getAuthenticatedUser();
|
|
114
|
+
if (!user) redirect('/login');
|
|
115
|
+
|
|
116
|
+
return <DashboardContent user={user} />;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### In Server Actions
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
'use server'
|
|
124
|
+
export async function updateProfile(formData: FormData) {
|
|
125
|
+
const user = await getAuthenticatedUser();
|
|
126
|
+
if (!user) throw new Error('Not authenticated');
|
|
127
|
+
|
|
128
|
+
// ... update logic
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Rule:** Check auth in EVERY server action and API route. Never trust the client.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Session Management
|
|
137
|
+
|
|
138
|
+
### Cookie-Based Sessions (Default)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Set session cookie
|
|
142
|
+
cookies().set('session', token, {
|
|
143
|
+
httpOnly: true, // JavaScript can't read it
|
|
144
|
+
secure: true, // HTTPS only
|
|
145
|
+
sameSite: 'lax', // CSRF protection
|
|
146
|
+
maxAge: 60 * 60 * 24 * 7, // 7 days
|
|
147
|
+
path: '/',
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Rules:**
|
|
152
|
+
- Always `httpOnly` — prevents XSS from stealing sessions
|
|
153
|
+
- Always `secure` in production
|
|
154
|
+
- Session expiry: 7 days for "remember me", 24 hours otherwise
|
|
155
|
+
- Rotate session token on privilege changes (login, password change)
|
|
156
|
+
|
|
157
|
+
### Never Use localStorage for Auth Tokens
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
BAD: localStorage.setItem('token', jwt) // XSS can steal it
|
|
161
|
+
GOOD: httpOnly cookie set by server // JavaScript can't access it
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## OAuth (Social Login)
|
|
167
|
+
|
|
168
|
+
### When to Add
|
|
169
|
+
|
|
170
|
+
- When users expect it (consumer apps)
|
|
171
|
+
- When you want to reduce signup friction
|
|
172
|
+
- NOT for B2B SaaS (email + password is fine)
|
|
173
|
+
|
|
174
|
+
### Common Providers
|
|
175
|
+
|
|
176
|
+
| Provider | Use When |
|
|
177
|
+
|----------|----------|
|
|
178
|
+
| Google | Default choice, everyone has an account |
|
|
179
|
+
| GitHub | Developer-facing products |
|
|
180
|
+
| Apple | iOS apps (required by App Store if you have social login) |
|
|
181
|
+
|
|
182
|
+
### Implementation Pattern
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
Click "Login with Google" →
|
|
186
|
+
Redirect to Google OAuth →
|
|
187
|
+
Google redirects back with code →
|
|
188
|
+
Exchange code for tokens (server-side) →
|
|
189
|
+
Create or find user →
|
|
190
|
+
Create session →
|
|
191
|
+
Redirect to app
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Rules:**
|
|
195
|
+
- Always offer email + password alongside OAuth
|
|
196
|
+
- Handle "same email, different provider" gracefully
|
|
197
|
+
- Store provider info but don't depend on it
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Auth UI Patterns
|
|
202
|
+
|
|
203
|
+
### Login Page
|
|
204
|
+
|
|
205
|
+
- Email field (autofocus)
|
|
206
|
+
- Password field (with show/hide toggle)
|
|
207
|
+
- "Log in" button (primary)
|
|
208
|
+
- "Forgot password?" link
|
|
209
|
+
- "Don't have an account? Sign up" link
|
|
210
|
+
- OAuth buttons below (if applicable)
|
|
211
|
+
|
|
212
|
+
### Signup Page
|
|
213
|
+
|
|
214
|
+
- Email field
|
|
215
|
+
- Password field
|
|
216
|
+
- "Create account" button
|
|
217
|
+
- "Already have an account? Log in" link
|
|
218
|
+
- Terms of service checkbox (if legally required)
|
|
219
|
+
|
|
220
|
+
### Common Mistakes
|
|
221
|
+
|
|
222
|
+
- Don't put login and signup on the same page with tabs
|
|
223
|
+
- Don't require username during signup (let them set it later)
|
|
224
|
+
- Don't ask for unnecessary info at signup (name, phone, company)
|
|
225
|
+
- Don't disable the submit button without explaining why
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Session Checkpoints
|
|
230
|
+
|
|
231
|
+
### Session 2: Auth Foundation
|
|
232
|
+
- [ ] Signup creates account
|
|
233
|
+
- [ ] Login creates session
|
|
234
|
+
- [ ] Logout destroys session
|
|
235
|
+
- [ ] Protected routes redirect to login
|
|
236
|
+
|
|
237
|
+
### Session 4: Auth Complete
|
|
238
|
+
- [ ] Password reset works
|
|
239
|
+
- [ ] Session cookies are httpOnly and secure
|
|
240
|
+
- [ ] Auth checked in all server actions
|
|
241
|
+
- [ ] Rate limiting on login endpoint
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
*Last updated: Baton Protocol v3.1*
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database-design
|
|
3
|
+
description: >-
|
|
4
|
+
Database design patterns — schema design, normalization, indexes, migrations,
|
|
5
|
+
and query optimization. Load at Session 1-2 when setting up the database.
|
|
6
|
+
Use when designing tables, writing migrations, or optimizing slow queries.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Database Design Skill
|
|
10
|
+
|
|
11
|
+
> Get the schema right early. Fixing it later means migrating live data.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Schema Design (Session 1)
|
|
16
|
+
|
|
17
|
+
### Every Table Needs
|
|
18
|
+
|
|
19
|
+
```sql
|
|
20
|
+
CREATE TABLE items (
|
|
21
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
22
|
+
-- your columns here
|
|
23
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
24
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- UUID primary keys (not auto-increment — safer for distributed systems)
|
|
29
|
+
- `created_at` and `updated_at` on every table
|
|
30
|
+
- Auto-update `updated_at` with a trigger
|
|
31
|
+
|
|
32
|
+
### Naming Conventions
|
|
33
|
+
|
|
34
|
+
| Thing | Convention | Example |
|
|
35
|
+
|-------|-----------|---------|
|
|
36
|
+
| Tables | Plural, snake_case | `order_items` |
|
|
37
|
+
| Columns | Singular, snake_case | `first_name` |
|
|
38
|
+
| Foreign keys | `{table_singular}_id` | `user_id` |
|
|
39
|
+
| Booleans | `is_` or `has_` prefix | `is_active` |
|
|
40
|
+
| Timestamps | `_at` suffix | `deleted_at` |
|
|
41
|
+
| Money | `_cents` or `_fils` suffix | `price_cents` |
|
|
42
|
+
|
|
43
|
+
### Data Types
|
|
44
|
+
|
|
45
|
+
| Use | Type | Why |
|
|
46
|
+
|-----|------|-----|
|
|
47
|
+
| IDs | UUID | No guessing, safe to expose |
|
|
48
|
+
| Short text | TEXT | PostgreSQL treats VARCHAR and TEXT the same |
|
|
49
|
+
| Long text | TEXT | Same as above |
|
|
50
|
+
| Money | INTEGER (cents) | No floating point errors |
|
|
51
|
+
| Booleans | BOOLEAN | Not integers |
|
|
52
|
+
| Timestamps | TIMESTAMPTZ | Always with timezone |
|
|
53
|
+
| JSON data | JSONB | Queryable, indexed |
|
|
54
|
+
| Enums | TEXT with check | More flexible than PostgreSQL ENUM |
|
|
55
|
+
|
|
56
|
+
```sql
|
|
57
|
+
-- Use TEXT with check instead of ENUM
|
|
58
|
+
status TEXT NOT NULL DEFAULT 'draft'
|
|
59
|
+
CHECK (status IN ('draft', 'active', 'archived'))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Relationships
|
|
65
|
+
|
|
66
|
+
### One-to-Many (Most Common)
|
|
67
|
+
|
|
68
|
+
```sql
|
|
69
|
+
-- A user has many posts
|
|
70
|
+
CREATE TABLE posts (
|
|
71
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
|
+
user_id UUID NOT NULL REFERENCES users(id),
|
|
73
|
+
title TEXT NOT NULL
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Many-to-Many
|
|
78
|
+
|
|
79
|
+
```sql
|
|
80
|
+
-- Users belong to many organizations
|
|
81
|
+
CREATE TABLE memberships (
|
|
82
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
83
|
+
user_id UUID NOT NULL REFERENCES users(id),
|
|
84
|
+
org_id UUID NOT NULL REFERENCES organizations(id),
|
|
85
|
+
role TEXT DEFAULT 'member',
|
|
86
|
+
UNIQUE(user_id, org_id)
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### When to Use JSONB Instead
|
|
91
|
+
|
|
92
|
+
Use JSONB for:
|
|
93
|
+
- Metadata that varies per record
|
|
94
|
+
- Settings/preferences
|
|
95
|
+
- Data you query rarely
|
|
96
|
+
|
|
97
|
+
Don't use JSONB for:
|
|
98
|
+
- Relationships (use foreign keys)
|
|
99
|
+
- Data you filter/sort by frequently
|
|
100
|
+
- Core business data
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Indexes (Session 2-3)
|
|
105
|
+
|
|
106
|
+
### Rules
|
|
107
|
+
|
|
108
|
+
1. **Foreign keys always get an index** — PostgreSQL doesn't auto-index them
|
|
109
|
+
2. **Columns you filter by get an index** — WHERE clauses need them
|
|
110
|
+
3. **Columns you sort by get an index** — ORDER BY is slow without them
|
|
111
|
+
4. **Don't index everything** — each index slows writes
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
-- Index foreign keys
|
|
115
|
+
CREATE INDEX idx_posts_user_id ON posts(user_id);
|
|
116
|
+
|
|
117
|
+
-- Index commonly filtered columns
|
|
118
|
+
CREATE INDEX idx_posts_status ON posts(status);
|
|
119
|
+
|
|
120
|
+
-- Composite index for common query patterns
|
|
121
|
+
CREATE INDEX idx_posts_user_status ON posts(user_id, status);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### When to Add
|
|
125
|
+
|
|
126
|
+
- Session 1-2: Index foreign keys
|
|
127
|
+
- Session 5+: Add indexes for slow queries (measure first)
|
|
128
|
+
- Don't add indexes "just in case"
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Migrations (Session 1+)
|
|
133
|
+
|
|
134
|
+
### Migration File Naming
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
20260214_001_create_users.sql
|
|
138
|
+
20260214_002_create_organizations.sql
|
|
139
|
+
20260215_001_add_status_to_users.sql
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Safe Migration Rules
|
|
143
|
+
|
|
144
|
+
**Safe (can run on live database):**
|
|
145
|
+
- Adding a table
|
|
146
|
+
- Adding a nullable column
|
|
147
|
+
- Adding an index (use CONCURRENTLY)
|
|
148
|
+
- Adding a column with a default
|
|
149
|
+
|
|
150
|
+
**Dangerous (plan carefully):**
|
|
151
|
+
- Renaming a column (breaks existing queries)
|
|
152
|
+
- Changing a column type
|
|
153
|
+
- Dropping a column (might break code)
|
|
154
|
+
- Dropping a table
|
|
155
|
+
|
|
156
|
+
**Never in production:**
|
|
157
|
+
- `DROP TABLE` without backup
|
|
158
|
+
- Changing column type with data in it
|
|
159
|
+
- Removing NOT NULL without checking data
|
|
160
|
+
|
|
161
|
+
### Migration Workflow
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
1. Write migration SQL
|
|
165
|
+
2. Test locally (reset database)
|
|
166
|
+
3. Ask user to run in production
|
|
167
|
+
4. Wait for confirmation
|
|
168
|
+
5. Regenerate types (if using TypeScript)
|
|
169
|
+
6. THEN build features for new tables
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Never build UI for tables that don't exist yet.**
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Query Patterns
|
|
177
|
+
|
|
178
|
+
### Fetching Lists
|
|
179
|
+
|
|
180
|
+
```sql
|
|
181
|
+
-- Always paginate, never SELECT * without LIMIT
|
|
182
|
+
SELECT id, name, status, created_at
|
|
183
|
+
FROM items
|
|
184
|
+
WHERE org_id = $1
|
|
185
|
+
ORDER BY created_at DESC
|
|
186
|
+
LIMIT 20 OFFSET 0;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Counting
|
|
190
|
+
|
|
191
|
+
```sql
|
|
192
|
+
-- For display ("42 items"), use count
|
|
193
|
+
SELECT COUNT(*) FROM items WHERE org_id = $1;
|
|
194
|
+
|
|
195
|
+
-- For "has any?", use EXISTS (faster)
|
|
196
|
+
SELECT EXISTS(SELECT 1 FROM items WHERE org_id = $1);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Soft Deletes
|
|
200
|
+
|
|
201
|
+
For important data (orders, financial records):
|
|
202
|
+
|
|
203
|
+
```sql
|
|
204
|
+
ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMPTZ;
|
|
205
|
+
|
|
206
|
+
-- "Delete" = set timestamp
|
|
207
|
+
UPDATE orders SET deleted_at = NOW() WHERE id = $1;
|
|
208
|
+
|
|
209
|
+
-- All queries exclude deleted
|
|
210
|
+
SELECT * FROM orders WHERE deleted_at IS NULL;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
For unimportant data: just delete it.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Common Mistakes
|
|
218
|
+
|
|
219
|
+
| Mistake | Fix |
|
|
220
|
+
|---------|-----|
|
|
221
|
+
| Using FLOAT for money | Use INTEGER (cents) |
|
|
222
|
+
| No indexes on foreign keys | Add after creating table |
|
|
223
|
+
| Building UI before migration runs | Wait for confirmation |
|
|
224
|
+
| Storing arrays as comma-separated strings | Use JSONB or junction table |
|
|
225
|
+
| Not testing migration rollback | Always have a rollback plan |
|
|
226
|
+
| Huge tables without pagination | Always LIMIT queries |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
*Last updated: Baton Protocol v3.1*
|