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.
@@ -0,0 +1,236 @@
1
+ ---
2
+ name: email
3
+ description: >-
4
+ Transactional email patterns — provider selection, template design,
5
+ deliverability, and common email flows (welcome, reset, notifications).
6
+ Load at Session 2-4 when the project needs to send emails to users.
7
+ ---
8
+
9
+ # Email Skill
10
+
11
+ > Users judge your app by your emails. Ugly emails = untrustworthy app.
12
+
13
+ ---
14
+
15
+ ## Choose a Provider (Session 2)
16
+
17
+ | Provider | Best For | Free Tier |
18
+ |----------|----------|-----------|
19
+ | **Resend** | Modern, great DX, React Email templates | 3,000/month |
20
+ | **SendGrid** | Established, high volume | 100/day |
21
+ | **Postmark** | Transactional focus, great deliverability | 100/month |
22
+ | **AWS SES** | Cheapest at scale, more setup | 62,000/month (from EC2) |
23
+
24
+ **Default:** Resend. Best developer experience, React Email for templates, generous free tier.
25
+
26
+ ---
27
+
28
+ ## Core Email Flows
29
+
30
+ ### Which Emails to Send (Priority Order)
31
+
32
+ | Email | When | Priority |
33
+ |-------|------|----------|
34
+ | **Welcome** | After signup | Must have |
35
+ | **Password reset** | User requests it | Must have |
36
+ | **Email verification** | After signup (if required) | Should have |
37
+ | **Transactional** | Order confirmation, receipt | Must have (if payments) |
38
+ | **Notification** | New activity, updates | Nice to have |
39
+
40
+ Build in this order. Don't start with marketing emails.
41
+
42
+ ---
43
+
44
+ ## Setup with Resend (Session 2-3)
45
+
46
+ ### Install
47
+
48
+ ```bash
49
+ npm install resend
50
+ ```
51
+
52
+ ### Send an Email
53
+
54
+ ```typescript
55
+ // lib/email.ts
56
+ import { Resend } from 'resend';
57
+
58
+ const resend = new Resend(process.env.RESEND_API_KEY);
59
+
60
+ export async function sendEmail({
61
+ to,
62
+ subject,
63
+ html,
64
+ }: {
65
+ to: string;
66
+ subject: string;
67
+ html: string;
68
+ }) {
69
+ const { error } = await resend.emails.send({
70
+ from: 'App Name <noreply@yourdomain.com>',
71
+ to,
72
+ subject,
73
+ html,
74
+ });
75
+
76
+ if (error) {
77
+ console.error('Email send failed:', error);
78
+ throw error;
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### Environment Variables
84
+
85
+ ```bash
86
+ RESEND_API_KEY=re_...
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Email Templates
92
+
93
+ ### Keep It Simple
94
+
95
+ Every email needs:
96
+ 1. **Logo/app name** at top
97
+ 2. **One clear message** (what happened)
98
+ 3. **One clear action** (button/link)
99
+ 4. **Footer** (company name, unsubscribe if marketing)
100
+
101
+ ### Welcome Email
102
+
103
+ ```
104
+ Subject: Welcome to [App Name]
105
+
106
+ Hi [Name],
107
+
108
+ Thanks for signing up.
109
+
110
+ Here's what you can do next:
111
+ → [One primary action button]
112
+
113
+ If you need help, just reply to this email.
114
+
115
+ — The [App Name] team
116
+ ```
117
+
118
+ ### Password Reset
119
+
120
+ ```
121
+ Subject: Reset your password
122
+
123
+ Hi,
124
+
125
+ Someone requested a password reset for your account.
126
+
127
+ [Reset Password button]
128
+
129
+ This link expires in 1 hour. If you didn't request this, ignore this email.
130
+ ```
131
+
132
+ ### Transactional (Order Confirmation)
133
+
134
+ ```
135
+ Subject: Order #1234 confirmed
136
+
137
+ Hi [Name],
138
+
139
+ Your order is confirmed.
140
+
141
+ [Order summary table]
142
+ Item 1 — $XX.XX
143
+ Item 2 — $XX.XX
144
+ Total — $XX.XX
145
+
146
+ We'll email you when it ships.
147
+
148
+ [View Order button]
149
+ ```
150
+
151
+ ---
152
+
153
+ ## React Email (If Using Resend)
154
+
155
+ ```typescript
156
+ // emails/welcome.tsx
157
+ import { Html, Head, Body, Container, Text, Button } from '@react-email/components';
158
+
159
+ export function WelcomeEmail({ name, url }: { name: string; url: string }) {
160
+ return (
161
+ <Html>
162
+ <Head />
163
+ <Body style={{ fontFamily: 'sans-serif', background: '#f9f9f9' }}>
164
+ <Container style={{ maxWidth: 480, margin: '0 auto', padding: 24 }}>
165
+ <Text style={{ fontSize: 20, fontWeight: 'bold' }}>
166
+ Welcome, {name}!
167
+ </Text>
168
+ <Text>Thanks for signing up. Get started:</Text>
169
+ <Button
170
+ href={url}
171
+ style={{
172
+ background: '#2563EB',
173
+ color: 'white',
174
+ padding: '12px 24px',
175
+ borderRadius: 6,
176
+ }}
177
+ >
178
+ Go to Dashboard
179
+ </Button>
180
+ </Container>
181
+ </Body>
182
+ </Html>
183
+ );
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Deliverability Basics
190
+
191
+ ### Required Setup
192
+
193
+ 1. **Custom domain** — send from `noreply@yourdomain.com`, not `@resend.dev`
194
+ 2. **DNS records** — add SPF, DKIM, and DMARC records (provider gives you these)
195
+ 3. **From name** — use your app name, not a person's name
196
+
197
+ ### Rules
198
+
199
+ - Don't send from `noreply@gmail.com` or free email providers
200
+ - Keep subject lines short (under 50 characters)
201
+ - Don't use spam trigger words (FREE, URGENT, ACT NOW)
202
+ - Always include a plain text version alongside HTML
203
+ - Test with email preview tools before sending
204
+
205
+ ---
206
+
207
+ ## Error Handling
208
+
209
+ ```typescript
210
+ // Emails should never crash your app
211
+ try {
212
+ await sendEmail({ to, subject, html });
213
+ } catch (error) {
214
+ // Log but don't block the user action
215
+ console.error('Failed to send email:', error);
216
+ // The user action (signup, order) should still succeed
217
+ }
218
+ ```
219
+
220
+ **Rule:** Email sending failures should be logged, not thrown. Never let a failed email block a user from completing an action.
221
+
222
+ ---
223
+
224
+ ## Common Mistakes
225
+
226
+ | Mistake | Fix |
227
+ |---------|-----|
228
+ | Sending from free email domain | Set up custom domain |
229
+ | No DNS records (SPF/DKIM) | Add them or emails go to spam |
230
+ | Blocking user actions on email failure | Send async, log errors |
231
+ | Building complex email templates early | Start with plain, styled text |
232
+ | Sending too many emails | Only send what users expect |
233
+
234
+ ---
235
+
236
+ *Last updated: Baton Protocol v3.1*
@@ -0,0 +1,216 @@
1
+ ---
2
+ name: file-uploads
3
+ description: >-
4
+ File upload patterns — storage strategy, direct uploads, presigned URLs,
5
+ image optimization, and validation. Load at Session 2-4 when the project
6
+ needs file uploads (avatars, images, documents). Use when implementing
7
+ any file upload feature.
8
+ ---
9
+
10
+ # File Uploads Skill
11
+
12
+ > Files are the messiest feature. Validate early, store smart, serve fast.
13
+
14
+ ---
15
+
16
+ ## Choose Storage (Session 1-2)
17
+
18
+ | Storage | Best For | Pricing |
19
+ |---------|----------|---------|
20
+ | **Supabase Storage** | Already using Supabase | 1GB free, then $0.021/GB |
21
+ | **Vercel Blob** | Simple, Next.js apps | 100MB free |
22
+ | **AWS S3** | Full control, large scale | Pay per GB |
23
+ | **Cloudflare R2** | S3-compatible, no egress fees | 10GB free |
24
+
25
+ **Default:** Use whatever your stack already includes. Supabase project? Use Supabase Storage. Don't add a new service just for files.
26
+
27
+ ---
28
+
29
+ ## Upload Flow (Session 2-3)
30
+
31
+ ### Direct Upload (Recommended)
32
+
33
+ ```
34
+ Browser → Presigned URL from server → Upload directly to storage → Save URL in database
35
+ ```
36
+
37
+ **Why direct:** Files don't pass through your server. Faster, cheaper, no server memory issues.
38
+
39
+ ### Server Upload (Simple but Limited)
40
+
41
+ ```
42
+ Browser → Server → Storage → Save URL in database
43
+ ```
44
+
45
+ **When to use:** Small files only (<5MB), when presigned URLs are overkill.
46
+
47
+ ### Presigned URL Pattern
48
+
49
+ ```typescript
50
+ // 1. Client requests upload URL
51
+ // POST /api/upload/request
52
+ 'use server'
53
+ export async function getUploadUrl(filename: string, contentType: string) {
54
+ const user = await getAuthenticatedUser();
55
+ if (!user) throw new Error('Not authenticated');
56
+
57
+ // Validate before generating URL
58
+ if (!ALLOWED_TYPES.includes(contentType)) {
59
+ throw new Error('Invalid file type');
60
+ }
61
+
62
+ const path = `${user.id}/${Date.now()}-${filename}`;
63
+ const { data, error } = await supabase.storage
64
+ .from('uploads')
65
+ .createSignedUploadUrl(path);
66
+
67
+ return { uploadUrl: data.signedUrl, path };
68
+ }
69
+
70
+ // 2. Client uploads directly to storage using the URL
71
+ // 3. Client sends the path back to save in database
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Validation (Always)
77
+
78
+ ### Before Upload
79
+
80
+ ```typescript
81
+ const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
82
+ const MAX_SIZE = 10 * 1024 * 1024; // 10MB
83
+
84
+ function validateFile(file: File): string | null {
85
+ if (!ALLOWED_TYPES.includes(file.type)) {
86
+ return 'File type not allowed';
87
+ }
88
+ if (file.size > MAX_SIZE) {
89
+ return 'File too large (max 10MB)';
90
+ }
91
+ return null; // valid
92
+ }
93
+ ```
94
+
95
+ ### What to Validate
96
+
97
+ | Check | Why |
98
+ |-------|-----|
99
+ | File type (MIME) | Prevent executable uploads |
100
+ | File size | Prevent storage abuse |
101
+ | Filename | Sanitize special characters |
102
+ | Dimensions (images) | Prevent absurdly large images |
103
+
104
+ ### Never Trust the Client
105
+
106
+ - Validate on server AND client
107
+ - Client validation is for UX (fast feedback)
108
+ - Server validation is for security (can't be bypassed)
109
+
110
+ ---
111
+
112
+ ## Image Optimization
113
+
114
+ ### On Upload
115
+
116
+ - Resize to max dimensions (e.g., 1920px wide for display images)
117
+ - Generate thumbnails (200x200 for avatars, 400x300 for cards)
118
+ - Convert to WebP (smaller, supported everywhere)
119
+
120
+ ### On Display
121
+
122
+ ```tsx
123
+ // Next.js Image handles optimization automatically
124
+ import Image from 'next/image';
125
+
126
+ <Image
127
+ src={imageUrl}
128
+ alt={description}
129
+ width={400}
130
+ height={300}
131
+ className="object-cover"
132
+ />
133
+ ```
134
+
135
+ **Rule:** Always use Next.js `<Image>` for user-uploaded images. It handles lazy loading, sizing, and format conversion.
136
+
137
+ ---
138
+
139
+ ## Avatar Upload Pattern
140
+
141
+ The most common file upload feature:
142
+
143
+ ```typescript
144
+ // Upload component
145
+ function AvatarUpload({ currentUrl }: { currentUrl?: string }) {
146
+ async function handleChange(e: ChangeEvent<HTMLInputElement>) {
147
+ const file = e.target.files?.[0];
148
+ if (!file) return;
149
+
150
+ const error = validateFile(file);
151
+ if (error) { alert(error); return; }
152
+
153
+ // Get presigned URL
154
+ const { uploadUrl, path } = await getUploadUrl(file.name, file.type);
155
+
156
+ // Upload directly to storage
157
+ await fetch(uploadUrl, { method: 'PUT', body: file });
158
+
159
+ // Save path in database
160
+ await updateAvatar(path);
161
+ }
162
+
163
+ return (
164
+ <label>
165
+ <img src={currentUrl || '/default-avatar.png'} />
166
+ <input type="file" accept="image/*" onChange={handleChange} hidden />
167
+ </label>
168
+ );
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Storage Organization
175
+
176
+ ### File Path Convention
177
+
178
+ ```
179
+ {user_id}/{purpose}/{timestamp}-{filename}
180
+
181
+ Examples:
182
+ abc123/avatars/1708300800-photo.jpg
183
+ abc123/documents/1708300800-contract.pdf
184
+ abc123/products/1708300800-hero-image.webp
185
+ ```
186
+
187
+ **Rules:**
188
+ - Always include user/org ID (for RLS and cleanup)
189
+ - Include timestamp (prevents collisions)
190
+ - Sanitize filename (remove special characters)
191
+
192
+ ---
193
+
194
+ ## Security
195
+
196
+ - Never serve user uploads from your main domain (use storage CDN URL)
197
+ - Set appropriate bucket policies (public for avatars, private for documents)
198
+ - Generate signed URLs for private files (expire in 1 hour)
199
+ - Scan for malware if handling sensitive documents (enterprise only)
200
+
201
+ ---
202
+
203
+ ## Common Mistakes
204
+
205
+ | Mistake | Fix |
206
+ |---------|-----|
207
+ | Storing files in database (BLOB) | Use object storage |
208
+ | No file size limit | Always set MAX_SIZE |
209
+ | Accepting any file type | Whitelist allowed MIME types |
210
+ | Uploading through server | Use presigned URLs for large files |
211
+ | Not handling upload failures | Show error, allow retry |
212
+ | No loading indicator | Show progress bar during upload |
213
+
214
+ ---
215
+
216
+ *Last updated: Baton Protocol v3.1*
@@ -0,0 +1,246 @@
1
+ ---
2
+ name: payments
3
+ description: >-
4
+ Payment integration patterns — Stripe Checkout, subscriptions, webhooks,
5
+ one-time payments, and billing portal. Load at Session 3-5 when the project
6
+ needs to accept payments. Use when discussing monetization or implementing
7
+ checkout.
8
+ ---
9
+
10
+ # Payments Skill
11
+
12
+ > Money code has zero margin for error. Use Stripe, don't build custom.
13
+
14
+ ---
15
+
16
+ ## Rule #1: Use Stripe Checkout
17
+
18
+ Don't build a custom payment form. Ever. Stripe Checkout handles:
19
+ - PCI compliance (you don't want this responsibility)
20
+ - Card input with validation
21
+ - Apple Pay, Google Pay, Link
22
+ - 3D Secure authentication
23
+ - Tax calculation
24
+ - Receipt emails
25
+ - 135+ currencies
26
+
27
+ ---
28
+
29
+ ## Choose Your Payment Model
30
+
31
+ | Model | Use When | Stripe Feature |
32
+ |-------|----------|---------------|
33
+ | **One-time** | Selling a product or service | Checkout (payment mode) |
34
+ | **Subscription** | Monthly/annual SaaS | Checkout (subscription mode) |
35
+ | **Usage-based** | Pay per API call, storage, etc. | Metered billing |
36
+ | **Freemium** | Free tier + paid upgrade | Checkout + feature gating |
37
+
38
+ **Default for SaaS:** Subscription with annual discount.
39
+ **Default for e-commerce:** One-time payments.
40
+
41
+ ---
42
+
43
+ ## One-Time Payment (Session 3-4)
44
+
45
+ ### Create Checkout Session
46
+
47
+ ```typescript
48
+ 'use server'
49
+ import Stripe from 'stripe';
50
+
51
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
52
+
53
+ export async function createCheckout(items: CartItem[]) {
54
+ const session = await stripe.checkout.sessions.create({
55
+ mode: 'payment',
56
+ line_items: items.map(item => ({
57
+ price_data: {
58
+ currency: 'usd',
59
+ product_data: { name: item.name },
60
+ unit_amount: item.priceCents,
61
+ },
62
+ quantity: item.quantity,
63
+ })),
64
+ success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
65
+ cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cart`,
66
+ });
67
+
68
+ redirect(session.url!);
69
+ }
70
+ ```
71
+
72
+ ### Success Page
73
+
74
+ ```typescript
75
+ // app/success/page.tsx
76
+ export default async function SuccessPage({ searchParams }) {
77
+ const sessionId = searchParams.session_id;
78
+
79
+ // Verify the session is real (don't trust the URL alone)
80
+ const session = await stripe.checkout.sessions.retrieve(sessionId);
81
+
82
+ if (session.payment_status !== 'paid') {
83
+ redirect('/cart');
84
+ }
85
+
86
+ return <div>Payment successful! Order confirmed.</div>;
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Subscription (Session 3-5)
93
+
94
+ ### Setup
95
+
96
+ 1. Create products and prices in Stripe Dashboard (not in code)
97
+ 2. Store Stripe `price_id` in your environment variables
98
+ 3. Create checkout session with subscription mode
99
+
100
+ ```typescript
101
+ const session = await stripe.checkout.sessions.create({
102
+ mode: 'subscription',
103
+ line_items: [{
104
+ price: process.env.STRIPE_PRO_PRICE_ID,
105
+ quantity: 1,
106
+ }],
107
+ customer_email: user.email,
108
+ success_url: `${baseUrl}/dashboard?upgraded=true`,
109
+ cancel_url: `${baseUrl}/pricing`,
110
+ });
111
+ ```
112
+
113
+ ### Customer Portal (Managing Subscriptions)
114
+
115
+ Don't build subscription management UI. Use Stripe's Customer Portal:
116
+
117
+ ```typescript
118
+ export async function createPortalSession(customerId: string) {
119
+ const session = await stripe.billingPortal.sessions.create({
120
+ customer: customerId,
121
+ return_url: `${process.env.NEXT_PUBLIC_APP_URL}/settings`,
122
+ });
123
+
124
+ redirect(session.url);
125
+ }
126
+ ```
127
+
128
+ This gives users: plan changes, cancellation, payment method updates, invoice history — all built by Stripe.
129
+
130
+ ---
131
+
132
+ ## Webhooks (Critical)
133
+
134
+ ### Setup
135
+
136
+ ```typescript
137
+ // app/api/webhooks/stripe/route.ts
138
+ export async function POST(req: Request) {
139
+ const body = await req.text();
140
+ const signature = req.headers.get('stripe-signature')!;
141
+
142
+ let event: Stripe.Event;
143
+ try {
144
+ event = stripe.webhooks.constructEvent(
145
+ body,
146
+ signature,
147
+ process.env.STRIPE_WEBHOOK_SECRET!
148
+ );
149
+ } catch (err) {
150
+ return Response.json({ error: 'Invalid signature' }, { status: 400 });
151
+ }
152
+
153
+ switch (event.type) {
154
+ case 'checkout.session.completed':
155
+ await handleCheckoutComplete(event.data.object);
156
+ break;
157
+ case 'customer.subscription.updated':
158
+ await handleSubscriptionUpdate(event.data.object);
159
+ break;
160
+ case 'customer.subscription.deleted':
161
+ await handleSubscriptionCancelled(event.data.object);
162
+ break;
163
+ case 'invoice.payment_failed':
164
+ await handlePaymentFailed(event.data.object);
165
+ break;
166
+ }
167
+
168
+ return Response.json({ received: true });
169
+ }
170
+ ```
171
+
172
+ ### Critical Rules
173
+
174
+ - **Always verify signatures** — never skip this
175
+ - **Return 200 quickly** — do heavy processing async
176
+ - **Handle idempotently** — webhooks can fire multiple times
177
+ - **Log every event** — you'll need this for debugging
178
+
179
+ ### Testing Webhooks Locally
180
+
181
+ ```bash
182
+ # Install Stripe CLI
183
+ stripe listen --forward-to localhost:3000/api/webhooks/stripe
184
+ ```
185
+
186
+ This gives you a temporary webhook secret for local development.
187
+
188
+ ---
189
+
190
+ ## Pricing Page (Session 3-4)
191
+
192
+ ### Standard Layout
193
+
194
+ ```
195
+ Free Pro Enterprise
196
+ $0/mo $29/mo Custom
197
+ ($290/year)
198
+
199
+ Feature 1 ✓ Feature 1 ✓ Feature 1 ✓
200
+ Feature 2 ✗ Feature 2 ✓ Feature 2 ✓
201
+ Feature 3 ✗ Feature 3 ✓ Feature 3 ✓
202
+ Feature 4 ✓ Feature 4 ✓
203
+ Feature 5 ✓
204
+
205
+ [Start Free] [Upgrade] [Contact Us]
206
+ ```
207
+
208
+ **Rules:**
209
+ - Highlight the recommended plan (usually middle)
210
+ - Show annual pricing with savings ("Save 20%")
211
+ - List features, not technical specs
212
+ - Primary CTA on the plan you want people to pick
213
+
214
+ ---
215
+
216
+ ## Environment Variables
217
+
218
+ ```bash
219
+ # .env.local
220
+ STRIPE_SECRET_KEY=sk_test_... # Server only
221
+ STRIPE_WEBHOOK_SECRET=whsec_... # Server only
222
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # Client OK
223
+
224
+ # Price IDs (from Stripe Dashboard)
225
+ STRIPE_PRO_MONTHLY_PRICE_ID=price_...
226
+ STRIPE_PRO_ANNUAL_PRICE_ID=price_...
227
+ ```
228
+
229
+ **Never expose the secret key to the client.**
230
+
231
+ ---
232
+
233
+ ## Common Mistakes
234
+
235
+ | Mistake | Fix |
236
+ |---------|-----|
237
+ | Building custom payment form | Use Stripe Checkout |
238
+ | Building subscription management UI | Use Stripe Customer Portal |
239
+ | Not verifying webhook signatures | Always verify |
240
+ | Trusting client-side payment confirmation | Verify via webhook |
241
+ | Hardcoding prices in UI | Pull from Stripe or env vars |
242
+ | Forgetting test mode vs live mode | Check keys before launch |
243
+
244
+ ---
245
+
246
+ *Last updated: Baton Protocol v3.1*