@blinkdotnew/sdk 0.17.4 → 0.18.1

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
+
351
+ ```typescript
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)
382
+
177
383
  ```typescript
178
- // Login/logout
179
- blink.auth.login(nextUrl?) // Redirect to auth page
180
- blink.auth.logout(redirectUrl?) // Clear tokens and redirect
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
+ ```
181
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)
@@ -285,7 +525,6 @@ await blink.db.todos.upsertMany([...])
285
525
  // Text generation (simple prompt)
286
526
  const { text } = await blink.ai.generateText({
287
527
  prompt: 'Write a poem about coding',
288
- model: 'gpt-4o-mini',
289
528
  maxTokens: 150
290
529
  })
291
530
 
@@ -306,6 +545,7 @@ const { text } = await blink.ai.generateText({
306
545
  // Text generation with image content
307
546
  // ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
308
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
309
549
  const { text } = await blink.ai.generateText({
310
550
  messages: [
311
551
  {
@@ -380,26 +620,97 @@ const { object: todoList } = await blink.ai.generateObject({
380
620
  // })
381
621
  // Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"
382
622
 
383
- // 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
384
626
 
385
627
  // Basic image generation - returns public URLs directly
386
628
  const { data } = await blink.ai.generateImage({
387
- prompt: 'A serene landscape with mountains and a lake at sunset',
388
- size: '1024x1024',
389
- quality: 'high',
390
- n: 2
629
+ prompt: 'A serene landscape with mountains and a lake at sunset'
391
630
  })
392
631
  console.log('Image URL:', data[0].url)
393
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
+
394
640
  // Image editing - transform existing images with prompts
395
641
  const { data: headshots } = await blink.ai.modifyImage({
396
- images: ['https://storage.example.com/user-photo.jpg'], // ... up to 16 images maximum!
397
- prompt: 'Generate professional business headshot with studio lighting for this person.',
398
- quality: 'high',
399
- 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'
400
713
  })
401
- // Options: size ('1024x1024', '1536x1024'), quality ('auto'|'low'|'medium'|'high'),
402
- // background ('auto'|'transparent'|'opaque'), n (1-10 images)
403
714
 
404
715
 
405
716
  // Speech synthesis
@@ -732,20 +1043,17 @@ try {
732
1043
  // Upload files (returns public URL directly)
733
1044
  const { publicUrl } = await blink.storage.upload(
734
1045
  file,
735
- 'path/to/file',
1046
+ `uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
736
1047
  {
737
1048
  upsert: true,
738
1049
  onProgress: (percent) => console.log(`${percent}%`)
739
1050
  }
740
1051
  )
741
- // If file is PNG, final path will be: path/to/file.png
742
1052
 
743
- // 💡 Pro tip: Use the actual filename (file.name) in your path to avoid confusion
744
- const { publicUrl } = await blink.storage.upload(
745
- file,
746
- `uploads/${file.name}`, // Uses actual filename with correct extension
747
- { upsert: true }
748
- )
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()}`)
749
1057
 
750
1058
  // Remove files
751
1059
  await blink.storage.remove('file1.jpg', 'file2.jpg')
@@ -1209,14 +1517,44 @@ try {
1209
1517
  ### Error Handling
1210
1518
 
1211
1519
  ```typescript
1212
- import { BlinkAuthError, BlinkAIError, BlinkStorageError, BlinkDataError, BlinkRealtimeError, BlinkNotificationsError } from '@blinkdotnew/sdk'
1213
-
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
1214
1531
  try {
1215
- const user = await blink.auth.me()
1532
+ const user = await blink.auth.signInWithEmail(email, password)
1216
1533
  } catch (error) {
1217
1534
  if (error instanceof BlinkAuthError) {
1218
- // Handle auth errors
1219
- 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)
1220
1558
  }
1221
1559
  }
1222
1560
  ```
@@ -1227,7 +1565,18 @@ try {
1227
1565
  const blink = createClient({
1228
1566
  projectId: 'your-project',
1229
1567
  baseUrl: 'https://custom-api.example.com',
1230
- 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
+ },
1231
1580
  httpClient: {
1232
1581
  timeout: 30000,
1233
1582
  retries: 3
@@ -1428,6 +1777,220 @@ function MyRealtimeComponent() {
1428
1777
 
1429
1778
  ### React
1430
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
+
1431
1994
  **⚠️ Critical: Always Use Auth State Listener, Never One-Time Checks**
1432
1995
 
1433
1996
  The most common authentication mistake is checking auth status once instead of listening to changes:
@@ -1458,62 +2021,47 @@ useEffect(() => {
1458
2021
  }, [])
1459
2022
  ```
1460
2023
 
1461
- **Why the one-time check fails:**
1462
- 1. App loads → `blink.auth.me()` called immediately
1463
- 2. Auth still initializing → Call fails, user set to `null`
1464
- 3. Auth completes later → App never knows because it only checked once
1465
- 4. User stuck on "Please sign in" screen forever
1466
-
1467
- **Always use `onAuthStateChanged()` for React apps!**
2024
+ #### Error Handling Example
1468
2025
 
1469
2026
  ```typescript
1470
- import { createClient } from '@blinkdotnew/sdk'
1471
- import { useState, useEffect } from 'react'
2027
+ import { BlinkAuthError, BlinkAuthErrorCode } from '@blinkdotnew/sdk'
1472
2028
 
1473
- const blink = createClient({ projectId: 'your-project', authRequired: true })
1474
-
1475
- function App() {
1476
- const [user, setUser] = useState(null)
1477
- const [loading, setLoading] = useState(true)
2029
+ function AuthForm() {
2030
+ const [error, setError] = useState('')
1478
2031
 
1479
- useEffect(() => {
1480
- const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1481
- setUser(state.user)
1482
- setLoading(state.isLoading)
1483
- })
1484
- return unsubscribe
1485
- }, [])
1486
-
1487
- if (loading) return <div>Loading...</div>
1488
- if (!user) return <div>Please log in</div>
1489
-
1490
- return <div>Welcome, {user.email}!</div>
1491
- }
1492
-
1493
- // WRONG - One-time auth check (misses auth state changes)
1494
- function BadApp() {
1495
- const [user, setUser] = useState(null)
1496
- const [loading, setLoading] = useState(true)
1497
-
1498
- useEffect(() => {
1499
- const checkAuth = async () => {
1500
- try {
1501
- const userData = await blink.auth.me()
1502
- setUser(userData)
1503
- } catch (error) {
1504
- console.error('Auth check failed:', error)
1505
- } finally {
1506
- 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
+ }
1507
2051
  }
1508
2052
  }
2053
+ }
1509
2054
 
1510
- checkAuth() // ❌ Only runs once - misses when auth completes later!
1511
- }, [])
1512
-
1513
- // This pattern causes users to get stuck on "Please sign in"
1514
- // even after authentication completes
2055
+ return (
2056
+ <div>
2057
+ {error && <div style={{ color: 'red' }}>{error}</div>}
2058
+ {/* Auth form */}
2059
+ </div>
2060
+ )
1515
2061
  }
2062
+ ```
1516
2063
 
2064
+ ```typescript
1517
2065
  // React example with search functionality
1518
2066
  function SearchResults() {
1519
2067
  const [query, setQuery] = useState('')
@@ -1749,7 +2297,11 @@ function RealtimeChat() {
1749
2297
  // pages/api/todos.ts
1750
2298
  import { createClient } from '@blinkdotnew/sdk'
1751
2299
 
1752
- const blink = createClient({ projectId: 'your-project', authRequired: false })
2300
+ // RECOMMENDED: Use new auth configuration
2301
+ const blink = createClient({
2302
+ projectId: 'your-project',
2303
+ auth: { mode: 'managed' } // Explicit configuration
2304
+ })
1753
2305
 
1754
2306
  export default async function handler(req, res) {
1755
2307
  const jwt = req.headers.authorization?.replace('Bearer ', '')
@@ -1765,7 +2317,11 @@ export default async function handler(req, res) {
1765
2317
  ```typescript
1766
2318
  import { createClient } from '@blinkdotnew/sdk'
1767
2319
 
1768
- const blink = createClient({ projectId: 'your-project', authRequired: false })
2320
+ // RECOMMENDED: Use new auth configuration
2321
+ const blink = createClient({
2322
+ projectId: 'your-project',
2323
+ auth: { mode: 'managed' } // Explicit configuration
2324
+ })
1769
2325
 
1770
2326
  Deno.serve(async (req) => {
1771
2327
  const jwt = req.headers.get('authorization')?.replace('Bearer ', '')