create-baton 1.0.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/README.md +58 -0
- package/bin/create-baton.js +9 -0
- package/package.json +36 -0
- package/src/constants.js +30 -0
- package/src/index.js +35 -0
- package/src/prompts.js +54 -0
- package/src/scaffold.js +193 -0
- package/templates/BATON_v3.1.md +849 -0
- package/templates/ide/CLAUDE.md.template +105 -0
- package/templates/ide/cursorrules.template +64 -0
- package/templates/skills/core/anti-overengineering/SKILL.md +180 -0
- package/templates/skills/core/cost-awareness/SKILL.md +207 -0
- package/templates/skills/core/launch-prep/SKILL.md +232 -0
- package/templates/skills/core/milestones/SKILL.md +167 -0
- package/templates/skills/core/production-readiness/SKILL.md +307 -0
- package/templates/skills/core/security/SKILL.md +309 -0
- package/templates/skills/core/testing/SKILL.md +307 -0
- package/templates/skills/core/ui-ux/SKILL.md +155 -0
- package/templates/skills/patterns/api-integration/SKILL.md +143 -0
- package/templates/skills/stacks/nextjs/SKILL.md +230 -0
- package/templates/skills/stacks/supabase/SKILL.md +402 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase
|
|
3
|
+
description: >-
|
|
4
|
+
Supabase patterns for production applications. Covers client setup, RLS
|
|
5
|
+
policies, database patterns, type generation, storage, auth, and
|
|
6
|
+
migrations. Use when working with Supabase, PostgreSQL via Supabase,
|
|
7
|
+
or when setting up backend with Supabase.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Supabase Skill File
|
|
11
|
+
|
|
12
|
+
> Proven patterns from production Supabase projects. Check here before searching the web.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
### Environment Variables
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# .env.local
|
|
22
|
+
NEXT_PUBLIC_SUPABASE_URL=your-project-url
|
|
23
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
|
24
|
+
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Server-only, never expose
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Client Setup
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// lib/supabase/client.ts - Browser client
|
|
31
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
32
|
+
|
|
33
|
+
export function createClient() {
|
|
34
|
+
return createBrowserClient(
|
|
35
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
36
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// lib/supabase/server.ts - Server client
|
|
43
|
+
import { createServerClient } from '@supabase/ssr'
|
|
44
|
+
import { cookies } from 'next/headers'
|
|
45
|
+
|
|
46
|
+
export async function createClient() {
|
|
47
|
+
const cookieStore = await cookies()
|
|
48
|
+
|
|
49
|
+
return createServerClient(
|
|
50
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
51
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
52
|
+
{
|
|
53
|
+
cookies: {
|
|
54
|
+
getAll() {
|
|
55
|
+
return cookieStore.getAll()
|
|
56
|
+
},
|
|
57
|
+
setAll(cookiesToSet) {
|
|
58
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
59
|
+
cookieStore.set(name, value, options)
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Row Level Security (RLS)
|
|
71
|
+
|
|
72
|
+
**MANDATORY:** Enable RLS on ALL tables. No exceptions.
|
|
73
|
+
|
|
74
|
+
### Basic Pattern
|
|
75
|
+
|
|
76
|
+
```sql
|
|
77
|
+
-- Enable RLS
|
|
78
|
+
ALTER TABLE items ENABLE ROW LEVEL SECURITY;
|
|
79
|
+
|
|
80
|
+
-- Users can only see their own items
|
|
81
|
+
CREATE POLICY "Users can view own items"
|
|
82
|
+
ON items FOR SELECT
|
|
83
|
+
USING (auth.uid() = user_id);
|
|
84
|
+
|
|
85
|
+
-- Users can only insert their own items
|
|
86
|
+
CREATE POLICY "Users can insert own items"
|
|
87
|
+
ON items FOR INSERT
|
|
88
|
+
WITH CHECK (auth.uid() = user_id);
|
|
89
|
+
|
|
90
|
+
-- Users can only update their own items
|
|
91
|
+
CREATE POLICY "Users can update own items"
|
|
92
|
+
ON items FOR UPDATE
|
|
93
|
+
USING (auth.uid() = user_id);
|
|
94
|
+
|
|
95
|
+
-- Users can only delete their own items
|
|
96
|
+
CREATE POLICY "Users can delete own items"
|
|
97
|
+
ON items FOR DELETE
|
|
98
|
+
USING (auth.uid() = user_id);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Service Role Bypass
|
|
102
|
+
|
|
103
|
+
For background jobs and admin operations, use service role key:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// lib/supabase/admin.ts - Admin client (server-only)
|
|
107
|
+
import { createClient } from '@supabase/supabase-js'
|
|
108
|
+
|
|
109
|
+
export const adminClient = createClient(
|
|
110
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
111
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY! // Bypasses RLS
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Warning:** Never expose service role key to client.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Database Patterns
|
|
120
|
+
|
|
121
|
+
### Currency Storage
|
|
122
|
+
|
|
123
|
+
Store currency in smallest units (cents, fils, etc.):
|
|
124
|
+
|
|
125
|
+
```sql
|
|
126
|
+
-- Store AED 100.50 as 10050 (fils)
|
|
127
|
+
amount_fils INTEGER NOT NULL
|
|
128
|
+
|
|
129
|
+
-- Store USD 99.99 as 9999 (cents)
|
|
130
|
+
amount_cents INTEGER NOT NULL
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Convert for display:
|
|
134
|
+
```typescript
|
|
135
|
+
const displayAmount = (fils: number) => (fils / 100).toFixed(2)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Timestamps
|
|
139
|
+
|
|
140
|
+
Always include audit timestamps:
|
|
141
|
+
|
|
142
|
+
```sql
|
|
143
|
+
CREATE TABLE items (
|
|
144
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
145
|
+
-- your fields
|
|
146
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
147
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
148
|
+
created_by UUID REFERENCES auth.users(id),
|
|
149
|
+
updated_by UUID REFERENCES auth.users(id)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
-- Auto-update updated_at
|
|
153
|
+
CREATE OR REPLACE FUNCTION update_updated_at()
|
|
154
|
+
RETURNS TRIGGER AS $$
|
|
155
|
+
BEGIN
|
|
156
|
+
NEW.updated_at = NOW();
|
|
157
|
+
RETURN NEW;
|
|
158
|
+
END;
|
|
159
|
+
$$ LANGUAGE plpgsql;
|
|
160
|
+
|
|
161
|
+
CREATE TRIGGER update_items_updated_at
|
|
162
|
+
BEFORE UPDATE ON items
|
|
163
|
+
FOR EACH ROW
|
|
164
|
+
EXECUTE FUNCTION update_updated_at();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Soft Deletes (Voiding)
|
|
168
|
+
|
|
169
|
+
Never hard-delete financial/audit records:
|
|
170
|
+
|
|
171
|
+
```sql
|
|
172
|
+
-- Add to table
|
|
173
|
+
is_void BOOLEAN DEFAULT FALSE,
|
|
174
|
+
voided_at TIMESTAMPTZ,
|
|
175
|
+
voided_by UUID REFERENCES auth.users(id),
|
|
176
|
+
void_reason TEXT
|
|
177
|
+
|
|
178
|
+
-- Update RLS to exclude voided records
|
|
179
|
+
CREATE POLICY "Users can view non-voided items"
|
|
180
|
+
ON items FOR SELECT
|
|
181
|
+
USING (auth.uid() = user_id AND is_void = FALSE);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Type Generation
|
|
187
|
+
|
|
188
|
+
Generate TypeScript types from your database:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx supabase gen types typescript --project-id your-project-id > src/types/database.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use in code:
|
|
195
|
+
```typescript
|
|
196
|
+
import { Database } from '@/types/database'
|
|
197
|
+
|
|
198
|
+
type Item = Database['public']['Tables']['items']['Row']
|
|
199
|
+
type InsertItem = Database['public']['Tables']['items']['Insert']
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Common Operations
|
|
205
|
+
|
|
206
|
+
### Fetch with Type Safety
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const { data, error } = await supabase
|
|
210
|
+
.from('items')
|
|
211
|
+
.select('*')
|
|
212
|
+
.eq('user_id', userId)
|
|
213
|
+
.order('created_at', { ascending: false })
|
|
214
|
+
|
|
215
|
+
if (error) throw error
|
|
216
|
+
// data is typed as Item[]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Insert with Returning
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const { data, error } = await supabase
|
|
223
|
+
.from('items')
|
|
224
|
+
.insert({ name: 'New Item', user_id: userId })
|
|
225
|
+
.select()
|
|
226
|
+
.single()
|
|
227
|
+
|
|
228
|
+
if (error) throw error
|
|
229
|
+
// data is the inserted row
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Update
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
const { error } = await supabase
|
|
236
|
+
.from('items')
|
|
237
|
+
.update({ name: 'Updated Name' })
|
|
238
|
+
.eq('id', itemId)
|
|
239
|
+
.eq('user_id', userId) // Always include user check
|
|
240
|
+
|
|
241
|
+
if (error) throw error
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Upsert
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const { data, error } = await supabase
|
|
248
|
+
.from('settings')
|
|
249
|
+
.upsert({
|
|
250
|
+
user_id: userId,
|
|
251
|
+
key: 'theme',
|
|
252
|
+
value: 'dark'
|
|
253
|
+
})
|
|
254
|
+
.select()
|
|
255
|
+
.single()
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Storage
|
|
261
|
+
|
|
262
|
+
### Bucket Setup
|
|
263
|
+
|
|
264
|
+
```sql
|
|
265
|
+
-- Create bucket (run in SQL editor)
|
|
266
|
+
INSERT INTO storage.buckets (id, name, public)
|
|
267
|
+
VALUES ('avatars', 'avatars', true);
|
|
268
|
+
|
|
269
|
+
-- RLS for storage
|
|
270
|
+
CREATE POLICY "Users can upload own avatar"
|
|
271
|
+
ON storage.objects FOR INSERT
|
|
272
|
+
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Upload File
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const { data, error } = await supabase.storage
|
|
279
|
+
.from('avatars')
|
|
280
|
+
.upload(`${userId}/${fileName}`, file)
|
|
281
|
+
|
|
282
|
+
// Get public URL
|
|
283
|
+
const { data: { publicUrl } } = supabase.storage
|
|
284
|
+
.from('avatars')
|
|
285
|
+
.getPublicUrl(`${userId}/${fileName}`)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Private Files (Signed URLs)
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// For private buckets
|
|
292
|
+
const { data, error } = await supabase.storage
|
|
293
|
+
.from('documents')
|
|
294
|
+
.createSignedUrl(`${userId}/${fileName}`, 3600) // 1 hour expiry
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Auth
|
|
300
|
+
|
|
301
|
+
### Get Current User (Server)
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
const supabase = await createClient()
|
|
305
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
306
|
+
|
|
307
|
+
if (!user) {
|
|
308
|
+
redirect('/login')
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Auth Helper
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// lib/auth.ts
|
|
316
|
+
export async function getAuthenticatedUser() {
|
|
317
|
+
const supabase = await createClient()
|
|
318
|
+
const { data: { user }, error } = await supabase.auth.getUser()
|
|
319
|
+
|
|
320
|
+
if (error || !user) {
|
|
321
|
+
throw new Error('Not authenticated')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return user
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Migrations
|
|
331
|
+
|
|
332
|
+
### Naming Convention
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
20260210_001_create_items_table.sql
|
|
336
|
+
20260210_002_add_status_to_items.sql
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Migration Workflow
|
|
340
|
+
|
|
341
|
+
1. Create migration file in `supabase/migrations/`
|
|
342
|
+
2. Test locally: `npx supabase db reset`
|
|
343
|
+
3. Ask user to run in production dashboard
|
|
344
|
+
4. Wait for confirmation
|
|
345
|
+
5. Regenerate types
|
|
346
|
+
6. Then build UI
|
|
347
|
+
|
|
348
|
+
**Never build UI for tables that don't exist yet.**
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Pitfalls to Avoid
|
|
353
|
+
|
|
354
|
+
### 1. Forgetting RLS
|
|
355
|
+
|
|
356
|
+
Every table needs RLS. No exceptions. Check with:
|
|
357
|
+
```sql
|
|
358
|
+
SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 2. Exposing Service Role Key
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
// BAD - service role in client code
|
|
365
|
+
'use client'
|
|
366
|
+
import { adminClient } from '@/lib/supabase/admin' // NEVER
|
|
367
|
+
|
|
368
|
+
// GOOD - use server actions
|
|
369
|
+
'use client'
|
|
370
|
+
import { adminAction } from '@/lib/actions/admin'
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### 3. Not Handling Errors
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// BAD
|
|
377
|
+
const { data } = await supabase.from('items').select('*')
|
|
378
|
+
|
|
379
|
+
// GOOD
|
|
380
|
+
const { data, error } = await supabase.from('items').select('*')
|
|
381
|
+
if (error) {
|
|
382
|
+
console.error('Failed to fetch items:', error)
|
|
383
|
+
throw error
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### 4. Using .single() Without Checking
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// BAD - throws if no rows
|
|
391
|
+
const { data } = await supabase.from('items').select('*').single()
|
|
392
|
+
|
|
393
|
+
// GOOD - handle empty case
|
|
394
|
+
const { data } = await supabase.from('items').select('*').maybeSingle()
|
|
395
|
+
if (!data) {
|
|
396
|
+
// Handle not found
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
*Last updated: Baton Protocol v3.1*
|