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.
@@ -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*