@blinkdotnew/sdk 0.17.3 → 0.18.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 CHANGED
@@ -27,10 +27,22 @@ import { createClient } from '@blinkdotnew/sdk'
27
27
 
28
28
  const blink = createClient({
29
29
  projectId: 'your-blink-project-id', // From blink.new dashboard
30
- authRequired: true
30
+ authRequired: false // Don't force immediate auth - let users browse first
31
31
  })
32
32
 
33
- // Authentication (built-in)
33
+ // Authentication (flexible) - Two modes available
34
+
35
+ // For websites: Let users browse, require auth for protected areas
36
+ if (!blink.auth.isAuthenticated() && needsAuth) {
37
+ blink.auth.login() // Redirects to blink.new auth page when needed
38
+ }
39
+
40
+ // For apps with custom auth UI (headless mode)
41
+ const user = await blink.auth.signUp({ email: 'user@example.com', password: 'secure123' })
42
+ const user = await blink.auth.signInWithEmail('user@example.com', 'secure123')
43
+ const user = await blink.auth.signInWithGoogle()
44
+
45
+ // Current user (works in both modes)
34
46
  const user = await blink.auth.me()
35
47
 
36
48
  // Database operations (zero config)
@@ -110,7 +122,7 @@ blink.analytics.enable()
110
122
  // Storage operations (instant - returns public URL directly)
111
123
  const { publicUrl } = await blink.storage.upload(
112
124
  file,
113
- `avatars/${user.id}.png`,
125
+ `avatars/${user.id}-${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
114
126
  { upsert: true }
115
127
  )
116
128
  ```
@@ -128,9 +140,9 @@ const { publicUrl } = await blink.storage.upload(
128
140
 
129
141
  This SDK powers every Blink-generated app with:
130
142
 
131
- - **🔐 Authentication**: JWT-based auth with automatic token management
143
+ - **🔐 Authentication**: Flexible auth system with managed (redirect) and headless (custom UI) modes, email/password, social providers (Google, GitHub, Apple, Microsoft), magic links, RBAC, and custom email branding
132
144
  - **🗄️ Database**: PostgREST-compatible CRUD operations with advanced filtering
133
- - **🤖 AI**: Text generation with web search, object generation, image creation, speech synthesis, and transcription
145
+ - **🤖 AI**: Text generation with web search, object generation, image creation (Gemini 2.5 Flash), speech synthesis, and transcription
134
146
  - **📄 Data**: Extract text content from documents, secure API proxy with secret substitution, web scraping, screenshots, and web search
135
147
  - **📁 Storage**: File upload, download, and management
136
148
  - **📧 Notifications**: Email sending with attachments, custom branding, and delivery tracking
@@ -152,10 +164,15 @@ import { createClient } from '@blinkdotnew/sdk'
152
164
 
153
165
  const blink = createClient({
154
166
  projectId: 'your-blink-project-id', // From blink.new dashboard
155
- authRequired: true // Automatic auth redirect
167
+ authRequired: false, // Let users browse first - require auth only for protected areas
168
+ auth: {
169
+ mode: 'managed' // Use new explicit configuration
170
+ }
156
171
  })
157
172
  ```
158
173
 
174
+ > **⚠️ Version Requirement**: The flexible authentication system (managed vs headless modes) requires SDK version **0.18.0 or higher**. If you're using version 0.17.x or below, you'll only have access to the legacy authentication system. Please upgrade to access the new authentication features.
175
+
159
176
  ### Server-side (Node.js, Deno, Edge functions)
160
177
 
161
178
  ```typescript
@@ -163,7 +180,7 @@ import { createClient } from '@blinkdotnew/sdk'
163
180
 
164
181
  const blink = createClient({
165
182
  projectId: 'your-blink-project-id', // From blink.new dashboard
166
- authRequired: false // Manual token management
183
+ auth: { mode: 'managed' } // Manual token management
167
184
  })
168
185
 
169
186
  // Set JWT manually
@@ -174,11 +191,224 @@ blink.auth.setToken(jwtFromHeader)
174
191
 
175
192
  ### Authentication
176
193
 
194
+ > **⚠️ Version Requirement**: The flexible authentication system requires SDK version **0.18.0 or higher**. Version 0.17.x and below only support the legacy authentication system.
195
+
196
+ Blink provides **two authentication modes** for maximum flexibility:
197
+
198
+ #### Managed Mode (Default - Redirect-based)
199
+
200
+ Perfect for quick setup with Blink's hosted auth page:
201
+
202
+ ```typescript
203
+ // ⚠️ DEPRECATED: authRequired is legacy - use auth.mode instead
204
+ const blink = createClient({
205
+ projectId: 'your-project',
206
+ authRequired: false // Legacy - still works but deprecated
207
+ })
208
+
209
+ // ✅ RECOMMENDED: Use new auth configuration
210
+ const blink = createClient({
211
+ projectId: 'your-project',
212
+ auth: {
213
+ mode: 'managed', // Explicit mode configuration
214
+ redirectUrl: 'https://myapp.com/dashboard'
215
+ }
216
+ })
217
+
218
+ // Simple redirect authentication
219
+ blink.auth.login() // Redirects to blink.new auth page
220
+ blink.auth.logout() // Clear tokens and redirect
221
+ ```
222
+
223
+ #### Headless Mode (Custom UI Control)
224
+
225
+ Build your own authentication UI with full control:
226
+
227
+ ```typescript
228
+ const blink = createClient({
229
+ projectId: 'your-project',
230
+ auth: {
231
+ mode: 'headless'
232
+ // Providers controlled via project settings
233
+ }
234
+ })
235
+
236
+ // Email/password authentication
237
+ const user = await blink.auth.signUp({
238
+ email: 'user@example.com',
239
+ password: 'SecurePass123!',
240
+ metadata: { displayName: 'John Doe' }
241
+ })
242
+
243
+ const user = await blink.auth.signInWithEmail('user@example.com', 'password')
244
+
245
+ // Social provider authentication
246
+ const user = await blink.auth.signInWithGoogle()
247
+ const user = await blink.auth.signInWithGitHub()
248
+ const user = await blink.auth.signInWithApple()
249
+ const user = await blink.auth.signInWithMicrosoft()
250
+
251
+ // Magic links (passwordless)
252
+ await blink.auth.sendMagicLink('user@example.com', {
253
+ redirectUrl: 'https://myapp.com/dashboard'
254
+ })
255
+ ```
256
+
257
+ #### 🔧 Provider Configuration
258
+
259
+ **Step 1: Enable Providers in Your Project**
260
+ 1. Go to [blink.new](https://blink.new) and open your project
261
+ 2. Navigate to **Project → Workspace → Authentication**
262
+ 3. Toggle providers on/off:
263
+ - **Email** ✅ (enabled by default) - Includes email/password, magic links, and verification
264
+ - **Google** ✅ (enabled by default)
265
+ - **GitHub** ⚪ (disabled by default)
266
+ - **Apple** ⚪ (disabled by default)
267
+ - **Microsoft** ⚪ (disabled by default)
268
+ 4. Configure email settings:
269
+ - **Require email verification**: Off by default (easier implementation)
270
+ - **Allow user signup**: On by default
271
+
272
+ **Step 2: Discover Available Providers**
273
+ ```typescript
274
+ // Get providers enabled for your project
275
+ const availableProviders = await blink.auth.getAvailableProviders()
276
+ // Returns: ['email', 'google'] (based on your project settings)
277
+
278
+ // Use in your UI to show only enabled providers
279
+ const showGoogleButton = availableProviders.includes('google')
280
+ const showGitHubButton = availableProviders.includes('github')
281
+ ```
282
+
283
+ **Step 3: Client-Side Filtering (Headless Mode)**
284
+ ```typescript
285
+ const blink = createClient({
286
+ projectId: 'your-project',
287
+ auth: {
288
+ mode: 'headless'
289
+ // All providers controlled via project settings
290
+ }
291
+ })
292
+ ```
293
+
294
+ **Managed Mode: Automatic Provider Display**
295
+ ```typescript
296
+ const blink = createClient({
297
+ projectId: 'your-project',
298
+ auth: { mode: 'managed' }
299
+ })
300
+
301
+ // The hosted auth page automatically shows only enabled providers
302
+ blink.auth.login() // Shows Email + Google by default
303
+ ```
304
+
305
+ #### 📧 Email Verification Flow
306
+
307
+ **By default, email verification is NOT required** for easier implementation. Enable it only if needed:
308
+
309
+ **Step 1: Configure Verification (Optional)**
310
+ 1. Go to [blink.new](https://blink.new) → Project → Workspace → Authentication
311
+ 2. Toggle **"Require email verification"** ON (disabled by default)
312
+
313
+ **Step 2: Handle the Complete Flow**
314
+ ```typescript
315
+ // User signup - always send verification email for security
316
+ try {
317
+ const user = await blink.auth.signUp({ email, password })
318
+ await blink.auth.sendEmailVerification()
319
+ setMessage('Account created! Check your email to verify.')
320
+ } catch (error) {
321
+ setError(error.message)
322
+ }
323
+
324
+ // User signin - handle verification requirement
325
+ try {
326
+ await blink.auth.signInWithEmail(email, password)
327
+ // Success - user is signed in
328
+ } catch (error) {
329
+ if (error.code === 'EMAIL_NOT_VERIFIED') {
330
+ setError('Please verify your email first')
331
+ await blink.auth.sendEmailVerification() // Resend verification
332
+ } else {
333
+ setError(error.message)
334
+ }
335
+ }
336
+
337
+ // Manual verification resend
338
+ await blink.auth.sendEmailVerification()
339
+ ```
340
+
341
+ **What Happens:**
342
+ 1. **Signup**: User account created, `email_verified = false`
343
+ 2. **Verification Email**: User clicks link → `email_verified = true`
344
+ 3. **Signin Check**: If verification required AND not verified → `EMAIL_NOT_VERIFIED` error
345
+ 4. **Success**: User can sign in once verified (or if verification not required)
346
+
347
+ #### Flexible Email System
348
+
349
+ **Maximum flexibility** - you control email branding while Blink handles secure tokens:
350
+
177
351
  ```typescript
178
- // Login/logout
179
- blink.auth.login(nextUrl?)
180
- blink.auth.logout(redirectUrl?)
352
+ // Option A: Custom email delivery (full branding control)
353
+ const resetData = await blink.auth.generatePasswordResetToken('user@example.com')
354
+ // Returns: { token, expiresAt, resetUrl }
355
+
356
+ // Send with your email service and branding
357
+ await yourEmailService.send({
358
+ to: 'user@example.com',
359
+ subject: 'Reset your YourApp password',
360
+ html: `
361
+ <div style="font-family: Arial, sans-serif;">
362
+ <img src="https://yourapp.com/logo.png" alt="YourApp" />
363
+ <h1>Reset Your Password</h1>
364
+ <a href="${resetData.resetUrl}"
365
+ style="background: #0070f3; color: white; padding: 16px 32px;
366
+ text-decoration: none; border-radius: 8px;">
367
+ Reset Password
368
+ </a>
369
+ </div>
370
+ `
371
+ })
372
+
373
+ // Option B: Blink default email (zero setup)
374
+ await blink.auth.sendPasswordResetEmail('user@example.com')
375
+
376
+ // Same flexibility for email verification and magic links
377
+ const verifyData = await blink.auth.generateEmailVerificationToken()
378
+ const magicData = await blink.auth.generateMagicLinkToken('user@example.com')
379
+ ```
380
+
381
+ #### Role-Based Access Control (RBAC)
181
382
 
383
+ ```typescript
384
+ // Configure roles and permissions
385
+ const blink = createClient({
386
+ projectId: 'your-project',
387
+ auth: {
388
+ mode: 'headless',
389
+ roles: {
390
+ admin: { permissions: ['*'] },
391
+ editor: { permissions: ['posts.create', 'posts.update'] },
392
+ viewer: { permissions: ['posts.read'] }
393
+ }
394
+ }
395
+ })
396
+
397
+ // Check permissions
398
+ const canEdit = blink.auth.can('posts.update')
399
+ const isAdmin = blink.auth.hasRole('admin')
400
+ const isStaff = blink.auth.hasRole(['admin', 'editor'])
401
+
402
+ // Use in components
403
+ function EditButton() {
404
+ if (!blink.auth.can('posts.update')) return null
405
+ return <button onClick={editPost}>Edit Post</button>
406
+ }
407
+ ```
408
+
409
+ #### Core Methods
410
+
411
+ ```typescript
182
412
  // User management
183
413
  const user = await blink.auth.me()
184
414
  await blink.auth.updateMe({ displayName: 'New Name' })
@@ -187,6 +417,16 @@ await blink.auth.updateMe({ displayName: 'New Name' })
187
417
  blink.auth.setToken(jwt, persist?)
188
418
  const isAuth = blink.auth.isAuthenticated()
189
419
 
420
+ // Password management
421
+ await blink.auth.changePassword('oldPass', 'newPass')
422
+ await blink.auth.confirmPasswordReset(token, newPassword)
423
+
424
+ // Email verification
425
+ await blink.auth.verifyEmail(token)
426
+
427
+ // Provider discovery
428
+ const providers = await blink.auth.getAvailableProviders()
429
+
190
430
  // Auth state listener (REQUIRED for React apps!)
191
431
  const unsubscribe = blink.auth.onAuthStateChanged((state) => {
192
432
  console.log('Auth state:', state)
@@ -197,6 +437,31 @@ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
197
437
  })
198
438
  ```
199
439
 
440
+ #### Login Redirect Behavior
441
+
442
+ When `login()` is called, the SDK automatically determines where to redirect after authentication:
443
+
444
+ ```typescript
445
+ // Automatic redirect (uses current page URL)
446
+ blink.auth.login()
447
+ // → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/current-page
448
+
449
+ // Custom redirect URL
450
+ blink.auth.login('https://yourapp.com/dashboard')
451
+ // → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/dashboard
452
+
453
+ // Manual login button example
454
+ const handleLogin = () => {
455
+ // The SDK will automatically use the current page URL
456
+ blink.auth.login()
457
+
458
+ // Or specify a custom redirect
459
+ // blink.auth.login('https://yourapp.com/welcome')
460
+ }
461
+ ```
462
+
463
+ **✅ Fixed in v1.x**: The SDK now ensures redirect URLs are always absolute, preventing broken redirects when `window.location.href` returns relative paths.
464
+
200
465
  ### Database Operations
201
466
 
202
467
  **🎉 NEW: Automatic Case Conversion!**
@@ -260,7 +525,6 @@ await blink.db.todos.upsertMany([...])
260
525
  // Text generation (simple prompt)
261
526
  const { text } = await blink.ai.generateText({
262
527
  prompt: 'Write a poem about coding',
263
- model: 'gpt-4o-mini',
264
528
  maxTokens: 150
265
529
  })
266
530
 
@@ -281,6 +545,7 @@ const { text } = await blink.ai.generateText({
281
545
  // Text generation with image content
282
546
  // ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
283
547
  // For file uploads, use blink.storage.upload() first to get public HTTPS URLs
548
+ // 🚫 CRITICAL: When uploading files, NEVER hardcode extensions - use file.name or auto-detection
284
549
  const { text } = await blink.ai.generateText({
285
550
  messages: [
286
551
  {
@@ -355,26 +620,97 @@ const { object: todoList } = await blink.ai.generateObject({
355
620
  // })
356
621
  // Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"
357
622
 
358
- // Generate and modify images with AI
623
+ // Generate and modify images with AI (powered by Gemini 2.5 Flash Image)
624
+ // 🔥 Automatic optimization - no need to specify size, quality, or style parameters
625
+ // 🎨 For style transfer: provide ALL images in the images array, don't reference URLs in prompts
359
626
 
360
627
  // Basic image generation - returns public URLs directly
361
628
  const { data } = await blink.ai.generateImage({
362
- prompt: 'A serene landscape with mountains and a lake at sunset',
363
- size: '1024x1024',
364
- quality: 'high',
365
- n: 2
629
+ prompt: 'A serene landscape with mountains and a lake at sunset'
366
630
  })
367
631
  console.log('Image URL:', data[0].url)
368
632
 
633
+ // Multiple images
634
+ const { data } = await blink.ai.generateImage({
635
+ prompt: 'A futuristic robot in different poses',
636
+ n: 3
637
+ })
638
+ data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url))
639
+
369
640
  // Image editing - transform existing images with prompts
370
641
  const { data: headshots } = await blink.ai.modifyImage({
371
- images: ['https://storage.example.com/user-photo.jpg'], // ... up to 16 images maximum!
372
- prompt: 'Generate professional business headshot with studio lighting for this person.',
373
- quality: 'high',
374
- n: 4
642
+ images: ['https://storage.example.com/user-photo.jpg'], // Up to 50 images supported!
643
+ prompt: 'Transform into professional business headshot with studio lighting'
644
+ })
645
+
646
+ // Advanced image editing with multiple input images
647
+ const { data } = await blink.ai.modifyImage({
648
+ images: [
649
+ 'https://storage.example.com/photo1.jpg',
650
+ 'https://storage.example.com/photo2.jpg',
651
+ 'https://storage.example.com/photo3.jpg'
652
+ ],
653
+ prompt: 'Combine these architectural styles into a futuristic building design',
654
+ n: 2
655
+ })
656
+
657
+ // 🎨 Style Transfer & Feature Application
658
+ // ⚠️ IMPORTANT: When applying styles/features from one image to another,
659
+ // provide ALL images in the array - don't reference images in the prompt text
660
+
661
+ // ❌ WRONG - Don't reference images in prompt
662
+ const wrongWay = await blink.ai.modifyImage({
663
+ images: [userPhotoUrl],
664
+ prompt: `Apply this hairstyle from the reference image to the person's head. Reference hairstyle: ${hairstyleUrl}`
665
+ })
666
+
667
+ // ✅ CORRECT - Provide all images in the array
668
+ const { data } = await blink.ai.modifyImage({
669
+ images: [userPhotoUrl, hairstyleUrl], // Both images provided
670
+ prompt: 'Apply the hairstyle from the second image to the person in the first image. Blend naturally with face shape.'
671
+ })
672
+
673
+ // More style transfer examples
674
+ const { data } = await blink.ai.modifyImage({
675
+ images: [portraitUrl, artworkUrl],
676
+ prompt: 'Apply the artistic style and color palette from the second image to the portrait in the first image'
677
+ })
678
+
679
+ const { data } = await blink.ai.modifyImage({
680
+ images: [roomPhotoUrl, designReferenceUrl],
681
+ prompt: 'Redesign the room in the first image using the interior design style shown in the second image'
682
+ })
683
+
684
+ // Multiple reference images for complex transformations
685
+ const { data } = await blink.ai.modifyImage({
686
+ images: [
687
+ originalPhotoUrl,
688
+ lightingReferenceUrl,
689
+ colorPaletteReferenceUrl,
690
+ compositionReferenceUrl
691
+ ],
692
+ prompt: 'Transform the first image using the lighting style from image 2, color palette from image 3, and composition from image 4'
693
+ })
694
+
695
+ // 📱 File Upload + Style Transfer
696
+ // ⚠️ Extract file extension properly - never hardcode .jpg/.png
697
+
698
+ // ❌ WRONG - Hardcoded extension
699
+ const userUpload = await blink.storage.upload(file, `photos/${Date.now()}.jpg`) // Breaks HEIC/PNG files
700
+ // ✅ CORRECT - Extract original extension
701
+ const userUpload = await blink.storage.upload(
702
+ userPhoto.file,
703
+ `photos/${Date.now()}.${userPhoto.file.name.split('.').pop()}`
704
+ )
705
+ const hairstyleUpload = await blink.storage.upload(
706
+ hairstylePhoto.file,
707
+ `haircuts/${Date.now()}.${hairstylePhoto.file.name.split('.').pop()}`
708
+ )
709
+
710
+ const { data } = await blink.ai.modifyImage({
711
+ images: [userUpload.publicUrl, hairstyleUpload.publicUrl],
712
+ prompt: 'Apply hairstyle from second image to person in first image'
375
713
  })
376
- // Options: size ('1024x1024', '1536x1024'), quality ('auto'|'low'|'medium'|'high'),
377
- // background ('auto'|'transparent'|'opaque'), n (1-10 images)
378
714
 
379
715
 
380
716
  // Speech synthesis
@@ -707,20 +1043,17 @@ try {
707
1043
  // Upload files (returns public URL directly)
708
1044
  const { publicUrl } = await blink.storage.upload(
709
1045
  file,
710
- 'path/to/file',
1046
+ `uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
711
1047
  {
712
1048
  upsert: true,
713
1049
  onProgress: (percent) => console.log(`${percent}%`)
714
1050
  }
715
1051
  )
716
- // If file is PNG, final path will be: path/to/file.png
717
1052
 
718
- // 💡 Pro tip: Use the actual filename (file.name) in your path to avoid confusion
719
- const { publicUrl } = await blink.storage.upload(
720
- file,
721
- `uploads/${file.name}`, // Uses actual filename with correct extension
722
- { upsert: true }
723
- )
1053
+ // WRONG - Hardcoded extensions break HEIC/PNG/WebP files
1054
+ const wrong = await blink.storage.upload(file, `uploads/${Date.now()}.jpg`) // Corrupts non-JPG files
1055
+ // ✅ CORRECT - Extract file extension
1056
+ const correct = await blink.storage.upload(file, `uploads/${Date.now()}.${file.name.split('.').pop()}`)
724
1057
 
725
1058
  // Remove files
726
1059
  await blink.storage.remove('file1.jpg', 'file2.jpg')
@@ -1184,14 +1517,44 @@ try {
1184
1517
  ### Error Handling
1185
1518
 
1186
1519
  ```typescript
1187
- import { BlinkAuthError, BlinkAIError, BlinkStorageError, BlinkDataError, BlinkRealtimeError, BlinkNotificationsError } from '@blinkdotnew/sdk'
1188
-
1520
+ import {
1521
+ BlinkAuthError,
1522
+ BlinkAuthErrorCode,
1523
+ BlinkAIError,
1524
+ BlinkStorageError,
1525
+ BlinkDataError,
1526
+ BlinkRealtimeError,
1527
+ BlinkNotificationsError
1528
+ } from '@blinkdotnew/sdk'
1529
+
1530
+ // Authentication error handling
1189
1531
  try {
1190
- const user = await blink.auth.me()
1532
+ const user = await blink.auth.signInWithEmail(email, password)
1191
1533
  } catch (error) {
1192
1534
  if (error instanceof BlinkAuthError) {
1193
- // Handle auth errors
1194
- console.error('Auth error:', error.message)
1535
+ switch (error.code) {
1536
+ case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
1537
+ console.log('Email verification required')
1538
+ await blink.auth.sendEmailVerification()
1539
+ break
1540
+ case BlinkAuthErrorCode.INVALID_CREDENTIALS:
1541
+ console.error('Invalid email or password')
1542
+ break
1543
+ case BlinkAuthErrorCode.RATE_LIMITED:
1544
+ console.error('Too many attempts, try again later')
1545
+ break
1546
+ default:
1547
+ console.error('Auth error:', error.message)
1548
+ }
1549
+ }
1550
+ }
1551
+
1552
+ // Other error types
1553
+ try {
1554
+ const { text } = await blink.ai.generateText({ prompt: 'Hello' })
1555
+ } catch (error) {
1556
+ if (error instanceof BlinkAIError) {
1557
+ console.error('AI error:', error.message)
1195
1558
  }
1196
1559
  }
1197
1560
  ```
@@ -1202,7 +1565,18 @@ try {
1202
1565
  const blink = createClient({
1203
1566
  projectId: 'your-project',
1204
1567
  baseUrl: 'https://custom-api.example.com',
1205
- authRequired: true,
1568
+ auth: {
1569
+ mode: 'headless', // 'managed' | 'headless'
1570
+ authUrl: 'https://custom-auth.example.com', // Custom auth domain
1571
+ coreUrl: 'https://custom-core.example.com', // Custom API domain
1572
+ // Providers controlled via project settings
1573
+ redirectUrl: 'https://myapp.com/dashboard',
1574
+ roles: {
1575
+ admin: { permissions: ['*'] },
1576
+ editor: { permissions: ['posts.create', 'posts.update'], inherit: ['viewer'] },
1577
+ viewer: { permissions: ['posts.read'] }
1578
+ }
1579
+ },
1206
1580
  httpClient: {
1207
1581
  timeout: 30000,
1208
1582
  retries: 3
@@ -1403,6 +1777,220 @@ function MyRealtimeComponent() {
1403
1777
 
1404
1778
  ### React
1405
1779
 
1780
+ #### Authentication Integration
1781
+
1782
+ **Complete authentication example with custom UI:**
1783
+
1784
+ ```typescript
1785
+ import { createClient } from '@blinkdotnew/sdk'
1786
+ import { useState, useEffect } from 'react'
1787
+
1788
+ // ✅ RECOMMENDED: Don't force immediate auth - let users browse first
1789
+ const blink = createClient({
1790
+ projectId: 'your-project',
1791
+ authRequired: false, // Let users browse public areas
1792
+ auth: {
1793
+ mode: 'headless' // Use headless for custom UI
1794
+ // Providers controlled via project settings
1795
+ }
1796
+ })
1797
+
1798
+ function App() {
1799
+ const [user, setUser] = useState(null)
1800
+ const [loading, setLoading] = useState(true)
1801
+ const [currentPage, setCurrentPage] = useState('home')
1802
+
1803
+ // ✅ CRITICAL: Always use onAuthStateChanged
1804
+ useEffect(() => {
1805
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1806
+ setUser(state.user)
1807
+ setLoading(state.isLoading)
1808
+ })
1809
+ return unsubscribe
1810
+ }, [])
1811
+
1812
+ // Protected route check - only require auth for dashboard/account areas
1813
+ const requiresAuth = ['dashboard', 'account', 'settings'].includes(currentPage)
1814
+
1815
+ if (loading) return <div>Loading...</div>
1816
+ if (requiresAuth && !user) return <AuthForm />
1817
+
1818
+ // Public pages (home, pricing, about) don't require auth
1819
+ if (currentPage === 'home') return <HomePage />
1820
+ if (currentPage === 'pricing') return <PricingPage />
1821
+
1822
+ // Protected pages require authentication
1823
+ return <Dashboard user={user} />
1824
+ }
1825
+
1826
+ function AuthForm() {
1827
+ const [mode, setMode] = useState('signin') // 'signin' | 'signup' | 'reset'
1828
+ const [email, setEmail] = useState('')
1829
+ const [password, setPassword] = useState('')
1830
+ const [error, setError] = useState('')
1831
+ const [message, setMessage] = useState('')
1832
+
1833
+ const handleEmailAuth = async (e) => {
1834
+ e.preventDefault()
1835
+ setError('')
1836
+
1837
+ try {
1838
+ if (mode === 'signup') {
1839
+ const user = await blink.auth.signUp({ email, password })
1840
+ setMessage('Account created! Please check your email to verify.')
1841
+ await blink.auth.sendEmailVerification()
1842
+ } else if (mode === 'signin') {
1843
+ await blink.auth.signInWithEmail(email, password)
1844
+ // User state updates automatically via onAuthStateChanged
1845
+ } else if (mode === 'reset') {
1846
+ await blink.auth.sendPasswordResetEmail(email)
1847
+ setMessage('Password reset email sent!')
1848
+ }
1849
+ } catch (err) {
1850
+ if (err.code === 'EMAIL_NOT_VERIFIED') {
1851
+ setError('Please verify your email first')
1852
+ await blink.auth.sendEmailVerification()
1853
+ } else {
1854
+ setError(err.message)
1855
+ }
1856
+ }
1857
+ }
1858
+
1859
+ const handleSocialAuth = async (provider) => {
1860
+ try {
1861
+ await blink.auth.signInWithProvider(provider)
1862
+ } catch (err) {
1863
+ setError(err.message)
1864
+ }
1865
+ }
1866
+
1867
+ return (
1868
+ <div style={{ maxWidth: '400px', margin: '0 auto', padding: '2rem' }}>
1869
+ <h1>{mode === 'signin' ? 'Sign In' : mode === 'signup' ? 'Sign Up' : 'Reset Password'}</h1>
1870
+
1871
+ {error && <div style={{ color: 'red', marginBottom: '1rem' }}>{error}</div>}
1872
+ {message && <div style={{ color: 'green', marginBottom: '1rem' }}>{message}</div>}
1873
+
1874
+ <form onSubmit={handleEmailAuth} style={{ marginBottom: '1rem' }}>
1875
+ <input
1876
+ type="email"
1877
+ placeholder="Email"
1878
+ value={email}
1879
+ onChange={(e) => setEmail(e.target.value)}
1880
+ style={{ width: '100%', padding: '0.5rem', marginBottom: '0.5rem' }}
1881
+ />
1882
+
1883
+ {mode !== 'reset' && (
1884
+ <input
1885
+ type="password"
1886
+ placeholder="Password"
1887
+ value={password}
1888
+ onChange={(e) => setPassword(e.target.value)}
1889
+ style={{ width: '100%', padding: '0.5rem', marginBottom: '1rem' }}
1890
+ />
1891
+ )}
1892
+
1893
+ <button type="submit" style={{ width: '100%', padding: '0.75rem', marginBottom: '1rem' }}>
1894
+ {mode === 'signin' ? 'Sign In' : mode === 'signup' ? 'Create Account' : 'Send Reset Email'}
1895
+ </button>
1896
+ </form>
1897
+
1898
+ {mode !== 'reset' && (
1899
+ <div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
1900
+ <button onClick={() => handleSocialAuth('google')} style={{ flex: 1, padding: '0.5rem' }}>
1901
+ Google
1902
+ </button>
1903
+ <button onClick={() => handleSocialAuth('github')} style={{ flex: 1, padding: '0.5rem' }}>
1904
+ GitHub
1905
+ </button>
1906
+ </div>
1907
+ )}
1908
+
1909
+ <div style={{ textAlign: 'center', fontSize: '0.875rem' }}>
1910
+ {mode === 'signin' && (
1911
+ <>
1912
+ <button onClick={() => setMode('signup')} style={{ marginRight: '1rem' }}>
1913
+ Create Account
1914
+ </button>
1915
+ <button onClick={() => setMode('reset')}>Forgot Password?</button>
1916
+ </>
1917
+ )}
1918
+ {mode === 'signup' && (
1919
+ <button onClick={() => setMode('signin')}>Already have an account?</button>
1920
+ )}
1921
+ {mode === 'reset' && (
1922
+ <button onClick={() => setMode('signin')}>Back to Sign In</button>
1923
+ )}
1924
+ </div>
1925
+ </div>
1926
+ )
1927
+ }
1928
+ ```
1929
+
1930
+ #### Custom Email Branding Example
1931
+
1932
+ **Send password reset with your own email service:**
1933
+
1934
+ ```typescript
1935
+ function PasswordResetForm() {
1936
+ const [email, setEmail] = useState('')
1937
+ const [message, setMessage] = useState('')
1938
+
1939
+ const handleReset = async (e) => {
1940
+ e.preventDefault()
1941
+
1942
+ try {
1943
+ // Generate secure token (no email sent by Blink)
1944
+ const resetData = await blink.auth.generatePasswordResetToken(email)
1945
+
1946
+ // Send with your own email service and branding
1947
+ await yourEmailService.send({
1948
+ to: email,
1949
+ from: 'support@yourapp.com',
1950
+ subject: 'Reset your YourApp password',
1951
+ html: `
1952
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 600px; margin: 0 auto;">
1953
+ <div style="text-align: center; padding: 40px;">
1954
+ <img src="https://yourapp.com/logo.svg" alt="YourApp" style="width: 120px; margin-bottom: 32px;" />
1955
+ <h1 style="color: #1a1a1a; font-size: 28px; margin-bottom: 16px;">Reset Your Password</h1>
1956
+ <p style="color: #666; font-size: 16px; margin-bottom: 32px;">
1957
+ We received a request to reset your YourApp password.
1958
+ </p>
1959
+ <a href="${resetData.resetUrl}"
1960
+ style="display: inline-block; background: #0070f3; color: white;
1961
+ padding: 16px 32px; text-decoration: none; border-radius: 8px;
1962
+ font-weight: 600; font-size: 16px;">
1963
+ Reset My Password
1964
+ </a>
1965
+ <p style="color: #999; font-size: 14px; margin-top: 32px;">
1966
+ This link expires in 1 hour. If you didn't request this, you can ignore this email.
1967
+ </p>
1968
+ </div>
1969
+ </div>
1970
+ `
1971
+ })
1972
+
1973
+ setMessage('Password reset email sent! Check your inbox.')
1974
+ } catch (error) {
1975
+ console.error('Reset failed:', error.message)
1976
+ }
1977
+ }
1978
+
1979
+ return (
1980
+ <form onSubmit={handleReset}>
1981
+ <input
1982
+ type="email"
1983
+ value={email}
1984
+ onChange={(e) => setEmail(e.target.value)}
1985
+ placeholder="Enter your email"
1986
+ />
1987
+ <button type="submit">Send Reset Link</button>
1988
+ {message && <p>{message}</p>}
1989
+ </form>
1990
+ )
1991
+ }
1992
+ ```
1993
+
1406
1994
  **⚠️ Critical: Always Use Auth State Listener, Never One-Time Checks**
1407
1995
 
1408
1996
  The most common authentication mistake is checking auth status once instead of listening to changes:
@@ -1433,61 +2021,45 @@ useEffect(() => {
1433
2021
  }, [])
1434
2022
  ```
1435
2023
 
1436
- **Why the one-time check fails:**
1437
- 1. App loads → `blink.auth.me()` called immediately
1438
- 2. Auth still initializing → Call fails, user set to `null`
1439
- 3. Auth completes later → App never knows because it only checked once
1440
- 4. User stuck on "Please sign in" screen forever
1441
-
1442
- **Always use `onAuthStateChanged()` for React apps!**
2024
+ #### Error Handling Example
1443
2025
 
1444
2026
  ```typescript
1445
- import { createClient } from '@blinkdotnew/sdk'
1446
- import { useState, useEffect } from 'react'
1447
-
1448
- const blink = createClient({ projectId: 'your-project', authRequired: true })
1449
-
1450
- function App() {
1451
- const [user, setUser] = useState(null)
1452
- const [loading, setLoading] = useState(true)
1453
-
1454
- useEffect(() => {
1455
- const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1456
- setUser(state.user)
1457
- setLoading(state.isLoading)
1458
- })
1459
- return unsubscribe
1460
- }, [])
1461
-
1462
- if (loading) return <div>Loading...</div>
1463
- if (!user) return <div>Please log in</div>
1464
-
1465
- return <div>Welcome, {user.email}!</div>
1466
- }
2027
+ import { BlinkAuthError, BlinkAuthErrorCode } from '@blinkdotnew/sdk'
1467
2028
 
1468
- // ❌ WRONG - One-time auth check (misses auth state changes)
1469
- function BadApp() {
1470
- const [user, setUser] = useState(null)
1471
- const [loading, setLoading] = useState(true)
2029
+ function AuthForm() {
2030
+ const [error, setError] = useState('')
1472
2031
 
1473
- useEffect(() => {
1474
- const checkAuth = async () => {
1475
- try {
1476
- const userData = await blink.auth.me()
1477
- setUser(userData)
1478
- } catch (error) {
1479
- console.error('Auth check failed:', error)
1480
- } finally {
1481
- setLoading(false)
2032
+ const handleAuth = async () => {
2033
+ try {
2034
+ await blink.auth.signInWithEmail(email, password)
2035
+ } catch (err) {
2036
+ if (err instanceof BlinkAuthError) {
2037
+ switch (err.code) {
2038
+ case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
2039
+ setError('Please verify your email first')
2040
+ await blink.auth.sendEmailVerification()
2041
+ break
2042
+ case BlinkAuthErrorCode.INVALID_CREDENTIALS:
2043
+ setError('Invalid email or password')
2044
+ break
2045
+ case BlinkAuthErrorCode.RATE_LIMITED:
2046
+ setError('Too many attempts. Please try again later.')
2047
+ break
2048
+ default:
2049
+ setError('Authentication failed. Please try again.')
2050
+ }
1482
2051
  }
1483
2052
  }
2053
+ }
1484
2054
 
1485
- checkAuth() // ❌ Only runs once - misses when auth completes later!
1486
- }, [])
1487
-
1488
- // This pattern causes users to get stuck on "Please sign in"
1489
- // even after authentication completes
2055
+ return (
2056
+ <div>
2057
+ {error && <div style={{ color: 'red' }}>{error}</div>}
2058
+ {/* Auth form */}
2059
+ </div>
2060
+ )
1490
2061
  }
2062
+ ```
1491
2063
 
1492
2064
  // React example with search functionality
1493
2065
  function SearchResults() {
@@ -1724,7 +2296,11 @@ function RealtimeChat() {
1724
2296
  // pages/api/todos.ts
1725
2297
  import { createClient } from '@blinkdotnew/sdk'
1726
2298
 
1727
- const blink = createClient({ projectId: 'your-project', authRequired: false })
2299
+ // RECOMMENDED: Use new auth configuration
2300
+ const blink = createClient({
2301
+ projectId: 'your-project',
2302
+ auth: { mode: 'managed' } // Explicit configuration
2303
+ })
1728
2304
 
1729
2305
  export default async function handler(req, res) {
1730
2306
  const jwt = req.headers.authorization?.replace('Bearer ', '')
@@ -1740,7 +2316,11 @@ export default async function handler(req, res) {
1740
2316
  ```typescript
1741
2317
  import { createClient } from '@blinkdotnew/sdk'
1742
2318
 
1743
- const blink = createClient({ projectId: 'your-project', authRequired: false })
2319
+ // RECOMMENDED: Use new auth configuration
2320
+ const blink = createClient({
2321
+ projectId: 'your-project',
2322
+ auth: { mode: 'managed' } // Explicit configuration
2323
+ })
1744
2324
 
1745
2325
  Deno.serve(async (req) => {
1746
2326
  const jwt = req.headers.get('authorization')?.replace('Bearer ', '')