@blinkdotnew/dev-sdk 0.0.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 ADDED
@@ -0,0 +1,2622 @@
1
+ # @blinkdotnew/sdk
2
+
3
+ [![npm version](https://badge.fury.io/js/%40blinkdotnew%2Fsdk.svg)](https://badge.fury.io/js/%40blinkdotnew%2Fsdk)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
5
+
6
+ **The full-stack TypeScript SDK that powers Blink AI-generated apps**
7
+
8
+ Blink is an AI App Builder that builds fully functional apps in seconds. This SDK (`@blinkdotnew/sdk`) is the TypeScript foundation that powers every Blink app natively, providing zero-boilerplate authentication, database operations, AI capabilities, and file storage. Works seamlessly on both client-side (React, Vue, etc.) and server-side (Node.js, Deno, Edge functions).
9
+
10
+ ## 🚀 Quick Start
11
+
12
+ ### **Step 1: Create a Blink Project**
13
+ Visit [blink.new](https://blink.new) and create a new project. Blink's AI agent will build your app in seconds.
14
+
15
+ ### **Step 2: Install the SDK**
16
+ Use Blink's AI agent to automatically install this SDK in your Vite React TypeScript client, or install manually:
17
+
18
+ ```bash
19
+ npm install @blinkdotnew/sdk
20
+ ```
21
+
22
+ ### **Step 3: Use Your Project ID**
23
+ Get your project ID from your Blink dashboard and start building:
24
+
25
+ ```typescript
26
+ import { createClient } from '@blinkdotnew/sdk'
27
+
28
+ const blink = createClient({
29
+ projectId: 'your-blink-project-id', // From blink.new dashboard
30
+ authRequired: false // Don't force immediate auth - let users browse first
31
+ })
32
+
33
+ // Authentication - Choose your mode:
34
+
35
+ // 🎯 MANAGED MODE: Quick setup with hosted auth page
36
+ const blink = createClient({
37
+ projectId: 'your-project',
38
+ auth: { mode: 'managed' }
39
+ })
40
+ // Use: blink.auth.login() - redirects to blink.new auth
41
+
42
+ // 🎨 HEADLESS MODE: Custom UI with full control
43
+ const blink = createClient({
44
+ projectId: 'your-project',
45
+ auth: { mode: 'headless' }
46
+ })
47
+ // Use: blink.auth.signInWithEmail(), blink.auth.signInWithGoogle(), etc.
48
+
49
+ // Current user (works in both modes)
50
+ const user = await blink.auth.me()
51
+
52
+ // Database operations (zero config)
53
+ const todos = await blink.db.todos.list({
54
+ where: { userId: user.id },
55
+ orderBy: { createdAt: 'desc' },
56
+ limit: 20
57
+ })
58
+
59
+ // AI operations (native)
60
+ const { text } = await blink.ai.generateText({
61
+ prompt: "Write a summary of the user's todos"
62
+ })
63
+
64
+ // Data operations (extract text from documents)
65
+ const text = await blink.data.extractFromUrl("https://example.com/document.pdf")
66
+
67
+ // Website scraping and screenshots (crystal clear results!)
68
+ const { markdown, metadata, links } = await blink.data.scrape("https://competitor.com")
69
+ const screenshotUrl = await blink.data.screenshot("https://competitor.com")
70
+
71
+ // Web search (get real-time information)
72
+ const searchResults = await blink.data.search("chatgpt latest news", { type: 'news' })
73
+ const localResults = await blink.data.search("best restaurants", { location: "San Francisco,CA,United States" })
74
+
75
+ // Notifications (NEW!)
76
+ const { success } = await blink.notifications.email({
77
+ to: 'customer@example.com',
78
+ subject: 'Your order has shipped!',
79
+ html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
80
+ })
81
+
82
+ // Secure API proxy (call external APIs with secret substitution)
83
+ const response = await blink.data.fetch({
84
+ url: "https://api.sendgrid.com/v3/mail/send",
85
+ method: "POST",
86
+ headers: { "Authorization": "Bearer {{sendgrid_api_key}}" },
87
+ body: { /* email data */ }
88
+ })
89
+
90
+ // Realtime operations (live messaging and presence)
91
+ const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
92
+ console.log('New message:', message.data)
93
+ })
94
+
95
+ await blink.realtime.publish('chat-room', 'message', { text: 'Hello world!' })
96
+
97
+ // Get presence - returns array of PresenceUser objects directly
98
+ const users = await blink.realtime.presence('chat-room')
99
+ console.log('Online users:', users.length)
100
+ // users is PresenceUser[] format:
101
+ // [
102
+ // {
103
+ // userId: 'user123',
104
+ // metadata: { displayName: 'Alice', status: 'online' },
105
+ // joinedAt: 1640995200000,
106
+ // lastSeen: 1640995230000
107
+ // }
108
+ // ]
109
+
110
+ // Analytics operations (automatic pageview tracking + custom events)
111
+ // Pageviews are tracked automatically on initialization and route changes
112
+ blink.analytics.log('button_clicked', {
113
+ button_id: 'signup',
114
+ page: '/pricing'
115
+ })
116
+
117
+ // Check if analytics is enabled
118
+ if (blink.analytics.isEnabled()) {
119
+ console.log('Analytics is active')
120
+ }
121
+
122
+ // Disable/enable analytics
123
+ blink.analytics.disable()
124
+ blink.analytics.enable()
125
+
126
+ // Storage operations (instant - returns public URL directly)
127
+ const { publicUrl } = await blink.storage.upload(
128
+ file,
129
+ `avatars/${user.id}-${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
130
+ { upsert: true }
131
+ )
132
+ ```
133
+
134
+ ## 🤖 What is Blink?
135
+
136
+ **Blink is an AI App Builder** that creates fully functional applications in seconds. Simply describe what you want to build, and Blink's AI agent will:
137
+
138
+ - 🏗️ **Generate complete apps** with React + TypeScript + Vite
139
+ - 🔧 **Auto-install this SDK** with zero configuration
140
+ - 🎨 **Create beautiful UIs** with Tailwind CSS
141
+ - 🚀 **Deploy instantly** with authentication, database, AI, and storage built-in
142
+
143
+ ## 📚 SDK Features
144
+
145
+ This SDK powers every Blink-generated app with:
146
+
147
+ - **🔐 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
148
+ - **🗄️ Database**: PostgREST-compatible CRUD operations with advanced filtering
149
+ - **🤖 AI**: Multi-model image generation & editing (10 models), text generation with web search, object generation, speech synthesis, and transcription
150
+ - **📄 Data**: Extract text content from documents, secure API proxy with secret substitution, web scraping, screenshots, and web search
151
+ - **📁 Storage**: File upload, download, and management
152
+ - **📧 Notifications**: Email sending with attachments, custom branding, and delivery tracking
153
+ - **⚡ Realtime**: WebSocket-based pub/sub messaging, presence tracking, and live updates
154
+ - **📊 Analytics**: Automatic pageview tracking, custom event logging, session management, and privacy-first design
155
+ - **🌐 Universal**: Works on client-side and server-side
156
+ - **📱 Framework Agnostic**: React, Vue, Svelte, vanilla JS, Node.js, Deno, **React Native**
157
+ - **📱 React Native**: First-class mobile support with AsyncStorage integration and platform-aware features
158
+ - **🔄 Real-time**: Built-in auth state management and token refresh
159
+ - **⚡ Zero Boilerplate**: Everything works out of the box
160
+
161
+ ## 🛠️ Manual Installation & Setup
162
+
163
+ > **💡 Tip**: If you're using Blink's AI agent, this is all done automatically for you!
164
+
165
+ ### Client-side (React, Vue, etc.)
166
+
167
+ ```typescript
168
+ import { createClient } from '@blinkdotnew/sdk'
169
+
170
+ const blink = createClient({
171
+ projectId: 'your-blink-project-id', // From blink.new dashboard
172
+ authRequired: false, // Let users browse first - require auth only for protected areas
173
+ auth: {
174
+ mode: 'managed' // Use new explicit configuration
175
+ }
176
+ })
177
+ ```
178
+
179
+ > **⚠️ 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.
180
+
181
+ ### Server-side (Node.js, Deno, Edge functions)
182
+
183
+ ```typescript
184
+ import { createClient } from '@blinkdotnew/sdk'
185
+
186
+ const blink = createClient({
187
+ projectId: 'your-blink-project-id', // From blink.new dashboard
188
+ auth: { mode: 'managed' } // Manual token management
189
+ })
190
+
191
+ // Token injection is only needed when calling blink.auth.* methods on the server
192
+ ```
193
+
194
+ ### 📱 React Native (iOS & Android)
195
+
196
+ The SDK has **first-class React Native support** with platform-aware features that automatically adapt to mobile environments.
197
+
198
+ #### Step 1: Install Dependencies
199
+
200
+ ```bash
201
+ npm install @blinkdotnew/sdk @react-native-async-storage/async-storage expo-web-browser
202
+ ```
203
+
204
+ **Required packages:**
205
+ - `@blinkdotnew/sdk` - The Blink SDK
206
+ - `@react-native-async-storage/async-storage` - For token persistence
207
+ - `expo-web-browser` - For OAuth authentication (Google, GitHub, Apple, etc.)
208
+
209
+ #### Step 2: Create Client (`lib/blink.ts`)
210
+
211
+ **⚠️ IMPORTANT: You MUST pass `webBrowser: WebBrowser` to enable OAuth on mobile!**
212
+
213
+ ```typescript
214
+ // lib/blink.ts
215
+ import { createClient, AsyncStorageAdapter } from '@blinkdotnew/sdk'
216
+ import AsyncStorage from '@react-native-async-storage/async-storage'
217
+ import * as WebBrowser from 'expo-web-browser' // ← Import this
218
+
219
+ export const blink = createClient({
220
+ projectId: 'your-project-id',
221
+ authRequired: false,
222
+ auth: {
223
+ mode: 'headless',
224
+ webBrowser: WebBrowser // ← Pass it here! Required for OAuth
225
+ },
226
+ storage: new AsyncStorageAdapter(AsyncStorage)
227
+ })
228
+ ```
229
+
230
+ #### Step 3: Use Authentication
231
+
232
+ **Email/Password (works immediately):**
233
+ ```typescript
234
+ // Sign up
235
+ const user = await blink.auth.signUp({
236
+ email: 'user@example.com',
237
+ password: 'SecurePass123'
238
+ })
239
+
240
+ // Sign in
241
+ const user = await blink.auth.signInWithEmail('user@example.com', 'SecurePass123')
242
+ ```
243
+
244
+ **OAuth (Google, GitHub, Apple, Microsoft):**
245
+ ```typescript
246
+ // ✅ Same code works on web, iOS, AND Android!
247
+ const user = await blink.auth.signInWithGoogle()
248
+ const user = await blink.auth.signInWithGitHub()
249
+ const user = await blink.auth.signInWithApple()
250
+ const user = await blink.auth.signInWithMicrosoft()
251
+ ```
252
+
253
+ #### Step 4: Create Auth Hook (`hooks/useAuth.ts`)
254
+
255
+ ```typescript
256
+ // hooks/useAuth.ts
257
+ import { useEffect, useState } from 'react'
258
+ import { blink } from '@/lib/blink'
259
+ import type { BlinkUser } from '@blinkdotnew/sdk'
260
+
261
+ export function useAuth() {
262
+ const [user, setUser] = useState<BlinkUser | null>(null)
263
+ const [isLoading, setIsLoading] = useState(true)
264
+
265
+ useEffect(() => {
266
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
267
+ setUser(state.user)
268
+ setIsLoading(state.isLoading)
269
+ })
270
+ return unsubscribe
271
+ }, [])
272
+
273
+ return {
274
+ user,
275
+ isLoading,
276
+ isAuthenticated: !!user,
277
+ signInWithGoogle: () => blink.auth.signInWithGoogle(),
278
+ signInWithGitHub: () => blink.auth.signInWithGitHub(),
279
+ signInWithApple: () => blink.auth.signInWithApple(),
280
+ signOut: () => blink.auth.signOut(),
281
+ }
282
+ }
283
+ ```
284
+
285
+ #### Step 5: Use in Components
286
+
287
+ ```typescript
288
+ import { useAuth } from '@/hooks/useAuth'
289
+ import { View, Text, Button } from 'react-native'
290
+
291
+ function App() {
292
+ const { user, isLoading, signInWithGoogle, signOut } = useAuth()
293
+
294
+ if (isLoading) return <Text>Loading...</Text>
295
+
296
+ if (!user) {
297
+ return <Button onPress={signInWithGoogle} title="Sign in with Google" />
298
+ }
299
+
300
+ return (
301
+ <View>
302
+ <Text>Welcome, {user.email}!</Text>
303
+ <Button onPress={signOut} title="Sign Out" />
304
+ </View>
305
+ )
306
+ }
307
+ ```
308
+
309
+ #### Common Mistakes
310
+
311
+ ```typescript
312
+ // ❌ WRONG: Missing webBrowser - OAuth won't work on mobile!
313
+ const blink = createClient({
314
+ projectId: 'your-project-id',
315
+ auth: { mode: 'headless' }, // Missing webBrowser!
316
+ storage: new AsyncStorageAdapter(AsyncStorage)
317
+ })
318
+
319
+ // ✅ CORRECT: Include webBrowser for OAuth support
320
+ import * as WebBrowser from 'expo-web-browser'
321
+
322
+ const blink = createClient({
323
+ projectId: 'your-project-id',
324
+ auth: {
325
+ mode: 'headless',
326
+ webBrowser: WebBrowser // ← Required for OAuth!
327
+ },
328
+ storage: new AsyncStorageAdapter(AsyncStorage)
329
+ })
330
+ ```
331
+
332
+ #### Low-Level OAuth (For Custom Control)
333
+
334
+ If you need custom polling timeouts or manual browser handling:
335
+
336
+ ```typescript
337
+ // Get auth URL and authenticate function separately
338
+ const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google')
339
+
340
+ // Open browser manually
341
+ await WebBrowser.openAuthSessionAsync(authUrl)
342
+
343
+ // Poll with custom options
344
+ const user = await authenticate({
345
+ maxAttempts: 120, // 60 seconds (default: 60 = 30 seconds)
346
+ intervalMs: 500 // Check every 500ms
347
+ })
348
+ ```
349
+
350
+ #### Platform Features
351
+
352
+ - ✅ **AsyncStorage** - Secure token persistence
353
+ - ✅ **Universal OAuth** - Same code works on web + mobile
354
+ - ✅ **expo-web-browser** - Native browser UI
355
+ - ✅ **No deep linking** - Session-based polling
356
+ - ✅ **Works in Expo Go** - No custom dev client needed
357
+ - ✅ **Auto token refresh** - Seamless sessions
358
+
359
+ ## 📖 API Reference
360
+
361
+ ### Authentication
362
+
363
+ > **⚠️ 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.
364
+
365
+ Blink provides **two authentication modes**:
366
+
367
+ ## 🎯 Managed Mode (Redirect-based)
368
+ **Perfect for:** Quick setup, minimal code
369
+ **Best for:** Websites, simple apps, MVP development
370
+
371
+ ```typescript
372
+ const blink = createClient({
373
+ projectId: 'your-project',
374
+ auth: { mode: 'managed' }
375
+ })
376
+
377
+ // ONE METHOD: Redirect to hosted auth page
378
+ blink.auth.login() // → Redirects to blink.new/auth
379
+ blink.auth.logout() // Clear tokens and redirect
380
+
381
+ // User state (automatic after redirect)
382
+ const user = await blink.auth.me()
383
+ ```
384
+
385
+ ## 🎨 Headless Mode (Custom UI)
386
+ **Perfect for:** Custom branding, advanced UX, mobile apps
387
+ **Best for:** Production apps, branded experiences
388
+
389
+ ```typescript
390
+ const blink = createClient({
391
+ projectId: 'your-project',
392
+ auth: { mode: 'headless' }
393
+ })
394
+
395
+ // MULTIPLE METHODS: Build your own UI
396
+ const user = await blink.auth.signUp({ email, password })
397
+ const user = await blink.auth.signInWithEmail(email, password)
398
+ const user = await blink.auth.signInWithGoogle()
399
+ const user = await blink.auth.signInWithGitHub()
400
+ const user = await blink.auth.signInWithApple()
401
+ const user = await blink.auth.signInWithMicrosoft()
402
+
403
+ // ✅ Store custom signup fields inside metadata
404
+ await blink.auth.signUp({
405
+ email: 'founder@example.com',
406
+ password: 'SuperSecret123',
407
+ displayName: 'Alex Founder',
408
+ role: 'operations',
409
+ metadata: {
410
+ company: 'Acme Freight',
411
+ marketingConsent: true
412
+ }
413
+ })
414
+ // `displayName`, `avatar`, and `role` map to dedicated auth columns.
415
+ // Everything else goes into auth.users.metadata automatically.
416
+ // Keep custom fields in metadata or your own profile table—avoid adding NOT NULL
417
+ // columns directly to auth tables.
418
+
419
+ // Magic links (passwordless)
420
+ await blink.auth.sendMagicLink(email)
421
+
422
+ // Password management
423
+ await blink.auth.sendPasswordResetEmail(email)
424
+ await blink.auth.sendPasswordResetEmail(email, {
425
+ redirectUrl: 'https://myapp.com/reset-password'
426
+ })
427
+ await blink.auth.changePassword(oldPass, newPass)
428
+
429
+ // Email verification
430
+ await blink.auth.sendEmailVerification()
431
+ ```
432
+
433
+ ### ⚡ Quick Mode Comparison
434
+
435
+ | Feature | **Managed Mode** | **Headless Mode** |
436
+ |---------|------------------|-------------------|
437
+ | **Setup** | 1 line of code | Custom UI required |
438
+ | **Methods** | `login()` only | `signInWith*()` methods |
439
+ | **UI** | Hosted auth page | Your custom forms |
440
+ | **Branding** | Blink-branded | Fully customizable |
441
+ | **Mobile** | Web redirects | Native integration |
442
+
443
+ ### 🚨 **Common Mistake**
444
+
445
+ ```typescript
446
+ // ❌ WRONG: Using managed method in headless mode
447
+ const blink = createClient({
448
+ auth: { mode: 'headless' }
449
+ })
450
+ blink.auth.login() // Still redirects! Wrong method for headless
451
+
452
+ // ✅ CORRECT: Use headless methods
453
+ await blink.auth.signInWithEmail(email, password)
454
+ await blink.auth.signInWithGoogle()
455
+ ```
456
+
457
+ #### 🔧 Provider Configuration
458
+
459
+ **Step 1: Enable Providers in Your Project**
460
+ 1. Go to [blink.new](https://blink.new) and open your project
461
+ 2. Navigate to **Project → Workspace → Authentication**
462
+ 3. Toggle providers on/off:
463
+ - **Email** ✅ (enabled by default) - Includes email/password, magic links, and verification
464
+ - **Google** ✅ (enabled by default)
465
+ - **GitHub** ⚪ (disabled by default)
466
+ - **Apple** ⚪ (disabled by default)
467
+ - **Microsoft** ⚪ (disabled by default)
468
+ 4. Configure email settings:
469
+ - **Require email verification**: Off by default (easier implementation)
470
+ - **Allow user signup**: On by default
471
+
472
+ **Step 2: Discover Available Providers**
473
+ ```typescript
474
+ // Get providers enabled for your project
475
+ const availableProviders = await blink.auth.getAvailableProviders()
476
+ // Returns: ['email', 'google'] (based on your project settings)
477
+
478
+ // Use in your UI to show only enabled providers
479
+ const showGoogleButton = availableProviders.includes('google')
480
+ const showGitHubButton = availableProviders.includes('github')
481
+ ```
482
+
483
+ **Step 3: Client-Side Filtering (Headless Mode)**
484
+ ```typescript
485
+ const blink = createClient({
486
+ projectId: 'your-project',
487
+ auth: {
488
+ mode: 'headless'
489
+ // All providers controlled via project settings
490
+ }
491
+ })
492
+ ```
493
+
494
+ **Managed Mode: Automatic Provider Display**
495
+ ```typescript
496
+ const blink = createClient({
497
+ projectId: 'your-project',
498
+ auth: { mode: 'managed' }
499
+ })
500
+
501
+ // The hosted auth page automatically shows only enabled providers
502
+ blink.auth.login() // Shows Email + Google by default
503
+ ```
504
+
505
+ #### 📧 Email Verification Flow
506
+
507
+ **By default, email verification is NOT required** for easier implementation. Enable it only if needed:
508
+
509
+ **Step 1: Configure Verification (Optional)**
510
+ 1. Go to [blink.new](https://blink.new) → Project → Workspace → Authentication
511
+ 2. Toggle **"Require email verification"** ON (disabled by default)
512
+
513
+ **Step 2: Handle the Complete Flow**
514
+ ```typescript
515
+ // User signup - always send verification email for security
516
+ try {
517
+ const user = await blink.auth.signUp({ email, password })
518
+ await blink.auth.sendEmailVerification()
519
+ setMessage('Account created! Check your email to verify.')
520
+ } catch (error) {
521
+ setError(error.message)
522
+ }
523
+
524
+ // User signin - handle verification requirement
525
+ try {
526
+ await blink.auth.signInWithEmail(email, password)
527
+ // Success - user is signed in
528
+ } catch (error) {
529
+ if (error.code === 'EMAIL_NOT_VERIFIED') {
530
+ setError('Please verify your email first')
531
+ await blink.auth.sendEmailVerification() // Resend verification
532
+ } else {
533
+ setError(error.message)
534
+ }
535
+ }
536
+
537
+ // Manual verification resend
538
+ await blink.auth.sendEmailVerification()
539
+ ```
540
+
541
+ **What Happens:**
542
+ 1. **Signup**: User account created, `email_verified = false`
543
+ 2. **Verification Email**: User clicks link → `email_verified = true`
544
+ 3. **Signin Check**: If verification required AND not verified → `EMAIL_NOT_VERIFIED` error
545
+ 4. **Success**: User can sign in once verified (or if verification not required)
546
+
547
+ #### Flexible Email System
548
+
549
+ **Maximum flexibility** - you control email branding while Blink handles secure tokens:
550
+
551
+ ```typescript
552
+ // Option A: Custom email delivery (full branding control)
553
+ const resetData = await blink.auth.generatePasswordResetToken('user@example.com')
554
+ // Returns: { token, expiresAt, resetUrl }
555
+
556
+ // Send with your email service and branding
557
+ await yourEmailService.send({
558
+ to: 'user@example.com',
559
+ subject: 'Reset your YourApp password',
560
+ html: `
561
+ <div style="font-family: Arial, sans-serif;">
562
+ <img src="https://yourapp.com/logo.png" alt="YourApp" />
563
+ <h1>Reset Your Password</h1>
564
+ <a href="${resetData.resetUrl}"
565
+ style="background: #0070f3; color: white; padding: 16px 32px;
566
+ text-decoration: none; border-radius: 8px;">
567
+ Reset Password
568
+ </a>
569
+ </div>
570
+ `
571
+ })
572
+
573
+ // Option B: Blink default email (zero setup)
574
+ await blink.auth.sendPasswordResetEmail('user@example.com')
575
+
576
+ // Same flexibility for email verification and magic links
577
+ const verifyData = await blink.auth.generateEmailVerificationToken()
578
+ const magicData = await blink.auth.generateMagicLinkToken('user@example.com')
579
+ ```
580
+
581
+ #### Role-Based Access Control (RBAC)
582
+
583
+ ```typescript
584
+ // Configure roles and permissions
585
+ const blink = createClient({
586
+ projectId: 'your-project',
587
+ auth: {
588
+ mode: 'headless',
589
+ roles: {
590
+ admin: { permissions: ['*'] },
591
+ editor: { permissions: ['posts.create', 'posts.update'] },
592
+ viewer: { permissions: ['posts.read'] }
593
+ }
594
+ }
595
+ })
596
+
597
+ // Check permissions
598
+ const canEdit = blink.auth.can('posts.update')
599
+ const isAdmin = blink.auth.hasRole('admin')
600
+ const isStaff = blink.auth.hasRole(['admin', 'editor'])
601
+
602
+ // Use in components
603
+ function EditButton() {
604
+ if (!blink.auth.can('posts.update')) return null
605
+ return <button onClick={editPost}>Edit Post</button>
606
+ }
607
+ ```
608
+
609
+ #### Core Methods
610
+
611
+ > ⚠️ Tokens are managed automatically for Blink APIs. Use `getValidToken()` only if you must manually pass a token to your own backend or third-party services.
612
+
613
+ ```typescript
614
+ // User management
615
+ const user = await blink.auth.me()
616
+ await blink.auth.updateMe({ displayName: 'New Name' })
617
+
618
+ // Token management
619
+ blink.auth.setToken(jwt, persist?)
620
+ const isAuth = blink.auth.isAuthenticated()
621
+ const token = await blink.auth.getValidToken() // Get valid token (auto-refreshes)
622
+
623
+ // Password management
624
+ await blink.auth.sendPasswordResetEmail('user@example.com')
625
+ await blink.auth.sendPasswordResetEmail('user@example.com', {
626
+ redirectUrl: 'https://myapp.com/reset-password' // Custom reset page
627
+ })
628
+ await blink.auth.changePassword('oldPass', 'newPass')
629
+ await blink.auth.confirmPasswordReset(token, newPassword)
630
+
631
+ // Email verification
632
+ await blink.auth.verifyEmail(token)
633
+
634
+ // Provider discovery
635
+ const providers = await blink.auth.getAvailableProviders()
636
+
637
+ // Auth state listener (REQUIRED for React apps!)
638
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
639
+ console.log('Auth state:', state)
640
+ // state.user - current user or null
641
+ // state.isLoading - true while auth is initializing
642
+ // state.isAuthenticated - true if user is logged in
643
+ // state.tokens - current auth tokens
644
+ })
645
+ ```
646
+
647
+ #### Login Redirect Behavior
648
+
649
+ When `login()` is called, the SDK automatically determines where to redirect after authentication:
650
+
651
+ ```typescript
652
+ // Automatic redirect (uses current page URL)
653
+ blink.auth.login()
654
+ // → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/current-page
655
+
656
+ // Custom redirect URL
657
+ blink.auth.login('https://yourapp.com/dashboard')
658
+ // → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/dashboard
659
+
660
+ // Manual login button example
661
+ const handleLogin = () => {
662
+ // The SDK will automatically use the current page URL
663
+ blink.auth.login()
664
+
665
+ // Or specify a custom redirect
666
+ // blink.auth.login('https://yourapp.com/welcome')
667
+ }
668
+ ```
669
+
670
+ **✅ Fixed in v1.x**: The SDK now ensures redirect URLs are always absolute, preventing broken redirects when `window.location.href` returns relative paths.
671
+
672
+ ### Database Operations
673
+
674
+ **🎉 NEW: Automatic Case Conversion!**
675
+ The SDK now automatically converts between JavaScript camelCase and SQL snake_case:
676
+ - **Table names**: `blink.db.emailDrafts` → `email_drafts` table
677
+ - **Field names**: `userId`, `createdAt`, `isCompleted` → `user_id`, `created_at`, `is_completed`
678
+ - **No manual conversion needed!**
679
+
680
+ **⚠️ Important: Always Use camelCase in Your Code**
681
+ - ✅ **Correct**: `blink.db.emailDrafts.create({ userId: user.id, createdAt: new Date() })`
682
+ - ❌ **Wrong**: `blink.db.email_drafts.create({ user_id: user.id, created_at: new Date() })`
683
+
684
+
685
+ ```typescript
686
+ // Create (ID auto-generated if not provided)
687
+ const todo = await blink.db.todos.create({
688
+ id: 'todo_12345', // Optional - auto-generated if not provided
689
+ title: 'Learn Blink SDK',
690
+ userId: user.id, // camelCase in code
691
+ createdAt: new Date(), // camelCase in code
692
+ isCompleted: false // camelCase in code
693
+ })
694
+
695
+ // Read with filtering - returns camelCase fields
696
+ const todos = await blink.db.todos.list({
697
+ where: {
698
+ AND: [
699
+ { userId: user.id }, // camelCase in filters
700
+ { OR: [{ status: 'open' }, { priority: 'high' }] }
701
+ ]
702
+ },
703
+ orderBy: { createdAt: 'desc' }, // camelCase in orderBy
704
+ limit: 20
705
+ })
706
+ // `todos` is a direct array: Todo[]
707
+
708
+ // Note: Boolean fields are returned as "0"/"1" strings from SQLite
709
+ // Check boolean values using Number(value) > 0
710
+ const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
711
+ const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)
712
+
713
+ // Update
714
+ await blink.db.todos.update(todo.id, { isCompleted: true })
715
+
716
+ // Delete
717
+ await blink.db.todos.delete(todo.id)
718
+
719
+ // Bulk operations (IDs auto-generated if not provided)
720
+ await blink.db.todos.createMany([
721
+ { title: 'Task 1', userId: user.id }, // ID will be auto-generated
722
+ { id: 'custom_id', title: 'Task 2', userId: user.id } // Custom ID provided
723
+ ])
724
+ await blink.db.todos.upsertMany([...])
725
+
726
+
727
+ ```
728
+
729
+ ### AI Operations
730
+
731
+ ```typescript
732
+ // Text generation (simple prompt)
733
+ const { text } = await blink.ai.generateText({
734
+ prompt: 'Write a poem about coding',
735
+ maxTokens: 150
736
+ })
737
+
738
+ // Web search (OpenAI only) - get real-time information
739
+ const { text, sources } = await blink.ai.generateText({
740
+ prompt: 'Who is the current US president?',
741
+ search: true // Returns current info + source URLs
742
+ })
743
+
744
+ // Multi-step reasoning - for complex analysis
745
+ const { text } = await blink.ai.generateText({
746
+ prompt: 'Research and analyze tech trends',
747
+ search: true,
748
+ maxSteps: 10, // Override default (25 when tools used)
749
+ experimental_continueSteps: true // Override default (true when tools used)
750
+ })
751
+
752
+ // Text generation with image content
753
+ // ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
754
+ // For file uploads, use blink.storage.upload() first to get public HTTPS URLs
755
+ // 🚫 CRITICAL: When uploading files, NEVER hardcode extensions - use file.name or auto-detection
756
+ const { text } = await blink.ai.generateText({
757
+ messages: [
758
+ {
759
+ role: "user",
760
+ content: [
761
+ { type: "text", text: "What do you see in this image?" },
762
+ { type: "image", image: "https://storage.googleapis.com/.../.../photo.jpg" }
763
+ ]
764
+ }
765
+ ]
766
+ })
767
+
768
+ // Mixed content with multiple images
769
+ const { text } = await blink.ai.generateText({
770
+ messages: [
771
+ {
772
+ role: "user",
773
+ content: [
774
+ { type: "text", text: "Compare these two images:" },
775
+ { type: "image", image: "https://storage.googleapis.com/.../.../image1.jpg" },
776
+ { type: "image", image: "https://cdn.example.com/image2.png" }
777
+ ]
778
+ }
779
+ ]
780
+ })
781
+
782
+ // Structured object generation
783
+ const { object } = await blink.ai.generateObject({
784
+ prompt: 'Generate a user profile',
785
+ schema: {
786
+ type: 'object',
787
+ properties: {
788
+ name: { type: 'string' },
789
+ age: { type: 'number' }
790
+ }
791
+ }
792
+ })
793
+
794
+ // ⚠️ IMPORTANT: Schema Rule for generateObject()
795
+ // The top-level schema MUST use type: "object" - you cannot use type: "array" at the top level
796
+ // This ensures clear, robust, and extensible API calls with named parameters
797
+
798
+ // ✅ Correct: Array inside object
799
+ const { object: todoList } = await blink.ai.generateObject({
800
+ prompt: 'Generate a list of 5 daily tasks',
801
+ schema: {
802
+ type: 'object',
803
+ properties: {
804
+ tasks: {
805
+ type: 'array',
806
+ items: {
807
+ type: 'object',
808
+ properties: {
809
+ title: { type: 'string' },
810
+ priority: { type: 'string', enum: ['low', 'medium', 'high'] }
811
+ }
812
+ }
813
+ }
814
+ },
815
+ required: ['tasks']
816
+ }
817
+ })
818
+ // Result: { tasks: [{ title: "Exercise", priority: "high" }, ...] }
819
+
820
+ // ❌ Wrong: Top-level array (will fail)
821
+ // const { object } = await blink.ai.generateObject({
822
+ // prompt: 'Generate tasks',
823
+ // schema: {
824
+ // type: 'array', // ❌ This will throw an error
825
+ // items: { type: 'string' }
826
+ // }
827
+ // })
828
+ // Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"
829
+
830
+ // Generate and modify images with AI - Multi-Model Support (10 models available)
831
+ // 🔥 Choose between fast generation or high-quality results
832
+ // 🎨 For style transfer: provide ALL images in the images array, don't reference URLs in prompts
833
+
834
+ // Basic image generation (uses default fast model: fal-ai/nano-banana)
835
+ const { data } = await blink.ai.generateImage({
836
+ prompt: 'A serene landscape with mountains and a lake at sunset'
837
+ })
838
+ console.log('Image URL:', data[0].url)
839
+
840
+ // High-quality generation with Pro model
841
+ const { data: proImage } = await blink.ai.generateImage({
842
+ prompt: 'A detailed infographic about AI with charts and diagrams',
843
+ model: 'fal-ai/nano-banana-pro', // High quality model
844
+ n: 1,
845
+ size: '1792x1024' // Custom size
846
+ })
847
+
848
+ // Generate multiple variations
849
+ const { data } = await blink.ai.generateImage({
850
+ prompt: 'A futuristic robot in different poses',
851
+ model: 'fal-ai/nano-banana', // Fast model
852
+ n: 3
853
+ })
854
+ data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url))
855
+
856
+ **Available Models for Text-to-Image:**
857
+
858
+ | Model | Speed | Quality | Best For |
859
+ |-------|-------|---------|----------|
860
+ | `fal-ai/nano-banana` (default) | ⚡ Fast | Good | Prototypes, high-volume generation |
861
+ | `fal-ai/nano-banana-pro` | Standard | ⭐ Excellent | Marketing materials, high-fidelity visuals |
862
+ | `fal-ai/gemini-25-flash-image` | ⚡ Fast | Good | Alias for `nano-banana` |
863
+ | `fal-ai/gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Alias for `nano-banana-pro` |
864
+ | `gemini-2.5-flash-image-preview` | ⚡ Fast | Good | Legacy - Direct Gemini API |
865
+ | `gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Legacy - Direct Gemini API |
866
+
867
+ // Image editing - transform existing images with prompts (uses default fast model)
868
+ const { data: headshots } = await blink.ai.modifyImage({
869
+ images: ['https://storage.example.com/user-photo.jpg'], // Up to 16 images supported!
870
+ prompt: 'Transform into professional business headshot with studio lighting'
871
+ })
872
+
873
+ // High-quality editing with Pro model
874
+ const { data: proEdited } = await blink.ai.modifyImage({
875
+ images: ['https://storage.example.com/portrait.jpg'],
876
+ prompt: 'Add a majestic ancient tree in the background with glowing leaves',
877
+ model: 'fal-ai/nano-banana-pro/edit' // High quality editing
878
+ })
879
+
880
+ // Advanced image editing with multiple input images
881
+ const { data } = await blink.ai.modifyImage({
882
+ images: [
883
+ 'https://storage.example.com/photo1.jpg',
884
+ 'https://storage.example.com/photo2.jpg',
885
+ 'https://storage.example.com/photo3.jpg'
886
+ ],
887
+ prompt: 'Combine these architectural styles into a futuristic building design',
888
+ model: 'fal-ai/nano-banana/edit', // Fast editing
889
+ n: 2
890
+ })
891
+
892
+ **Available Models for Image Editing:**
893
+
894
+ | Model | Speed | Quality | Best For |
895
+ |-------|-------|---------|----------|
896
+ | `fal-ai/nano-banana/edit` (default) | ⚡ Fast | Good | Quick adjustments, style transfers |
897
+ | `fal-ai/nano-banana-pro/edit` | Standard | ⭐ Excellent | Detailed retouching, complex edits |
898
+ | `fal-ai/gemini-25-flash-image/edit` | ⚡ Fast | Good | Alias for `nano-banana/edit` |
899
+ | `fal-ai/gemini-3-pro-image-preview/edit` | Standard | ⭐ Excellent | Alias for `nano-banana-pro/edit` |
900
+
901
+ // 🎨 Style Transfer & Feature Application
902
+ // ⚠️ IMPORTANT: When applying styles/features from one image to another,
903
+ // provide ALL images in the array - don't reference images in the prompt text
904
+
905
+ // ❌ WRONG - Don't reference images in prompt
906
+ const wrongWay = await blink.ai.modifyImage({
907
+ images: [userPhotoUrl],
908
+ prompt: `Apply this hairstyle from the reference image to the person's head. Reference hairstyle: ${hairstyleUrl}`
909
+ })
910
+
911
+ // ✅ CORRECT - Provide all images in the array
912
+ const { data } = await blink.ai.modifyImage({
913
+ images: [userPhotoUrl, hairstyleUrl], // Both images provided
914
+ prompt: 'Apply the hairstyle from the second image to the person in the first image. Blend naturally with face shape.'
915
+ })
916
+
917
+ // More style transfer examples
918
+ const { data } = await blink.ai.modifyImage({
919
+ images: [portraitUrl, artworkUrl],
920
+ prompt: 'Apply the artistic style and color palette from the second image to the portrait in the first image'
921
+ })
922
+
923
+ const { data } = await blink.ai.modifyImage({
924
+ images: [roomPhotoUrl, designReferenceUrl],
925
+ prompt: 'Redesign the room in the first image using the interior design style shown in the second image'
926
+ })
927
+
928
+ // Multiple reference images for complex transformations
929
+ const { data } = await blink.ai.modifyImage({
930
+ images: [
931
+ originalPhotoUrl,
932
+ lightingReferenceUrl,
933
+ colorPaletteReferenceUrl,
934
+ compositionReferenceUrl
935
+ ],
936
+ prompt: 'Transform the first image using the lighting style from image 2, color palette from image 3, and composition from image 4'
937
+ })
938
+
939
+ // 📱 File Upload + Style Transfer
940
+ // ⚠️ Extract file extension properly - never hardcode .jpg/.png
941
+
942
+ // ❌ WRONG - Hardcoded extension
943
+ const userUpload = await blink.storage.upload(file, `photos/${Date.now()}.jpg`) // Breaks HEIC/PNG files
944
+ // ✅ CORRECT - Extract original extension
945
+ const userUpload = await blink.storage.upload(
946
+ userPhoto.file,
947
+ `photos/${Date.now()}.${userPhoto.file.name.split('.').pop()}`
948
+ )
949
+ const hairstyleUpload = await blink.storage.upload(
950
+ hairstylePhoto.file,
951
+ `haircuts/${Date.now()}.${hairstylePhoto.file.name.split('.').pop()}`
952
+ )
953
+
954
+ const { data } = await blink.ai.modifyImage({
955
+ images: [userUpload.publicUrl, hairstyleUpload.publicUrl],
956
+ prompt: 'Apply hairstyle from second image to person in first image'
957
+ })
958
+
959
+
960
+ // Speech synthesis
961
+ const { url } = await blink.ai.generateSpeech({
962
+ text: 'Hello, world!',
963
+ voice: 'nova'
964
+ })
965
+
966
+ // Audio transcription - Multiple input formats supported
967
+ // 🔥 Most common: Browser audio recording → Base64 → Transcription
968
+ let mediaRecorder: MediaRecorder;
969
+ let audioChunks: Blob[] = [];
970
+
971
+ // Step 1: Start recording
972
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
973
+ mediaRecorder = new MediaRecorder(stream);
974
+
975
+ mediaRecorder.ondataavailable = (event) => {
976
+ audioChunks.push(event.data);
977
+ };
978
+
979
+ mediaRecorder.start();
980
+
981
+ // Step 2: Stop recording and transcribe
982
+ mediaRecorder.onstop = async () => {
983
+ const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
984
+
985
+ // SAFE method for large files - use FileReader (recommended)
986
+ const base64 = await new Promise<string>((resolve, reject) => {
987
+ const reader = new FileReader();
988
+ reader.onload = () => {
989
+ const dataUrl = reader.result as string;
990
+ const base64Data = dataUrl.split(',')[1]; // Extract base64 part
991
+ resolve(base64Data);
992
+ };
993
+ reader.onerror = reject;
994
+ reader.readAsDataURL(audioBlob);
995
+ });
996
+
997
+ // Transcribe using base64 (preferred method)
998
+ const { text } = await blink.ai.transcribeAudio({
999
+ audio: base64, // Raw base64 string
1000
+ language: 'en'
1001
+ });
1002
+
1003
+ console.log('Transcription:', text);
1004
+ audioChunks = []; // Reset for next recording
1005
+ };
1006
+
1007
+ // Alternative: Data URL format (also supported)
1008
+ const reader = new FileReader();
1009
+ reader.onload = async () => {
1010
+ const dataUrl = reader.result as string;
1011
+ const { text } = await blink.ai.transcribeAudio({
1012
+ audio: dataUrl, // Data URL format
1013
+ language: 'en'
1014
+ });
1015
+ };
1016
+ reader.readAsDataURL(audioBlob);
1017
+
1018
+ // File upload transcription
1019
+ const fileInput = document.getElementById('audioFile') as HTMLInputElement;
1020
+ const file = fileInput.files[0];
1021
+
1022
+ // Option 1: Convert to base64 using FileReader (recommended for large files)
1023
+ const base64Audio = await new Promise<string>((resolve, reject) => {
1024
+ const reader = new FileReader();
1025
+ reader.onload = () => {
1026
+ const dataUrl = reader.result as string;
1027
+ const base64Data = dataUrl.split(',')[1]; // Extract base64 part
1028
+ resolve(base64Data);
1029
+ };
1030
+ reader.onerror = reject;
1031
+ reader.readAsDataURL(file);
1032
+ });
1033
+
1034
+ const { text } = await blink.ai.transcribeAudio({
1035
+ audio: base64Audio,
1036
+ language: 'en'
1037
+ });
1038
+
1039
+ // Option 2: Use ArrayBuffer directly (works for any file size)
1040
+ const arrayBuffer = await file.arrayBuffer();
1041
+ const { text } = await blink.ai.transcribeAudio({
1042
+ audio: arrayBuffer,
1043
+ language: 'en'
1044
+ });
1045
+
1046
+ // Option 3: Use Uint8Array directly (works for any file size)
1047
+ const arrayBuffer = await file.arrayBuffer();
1048
+ const uint8Array = new Uint8Array(arrayBuffer);
1049
+ const { text } = await blink.ai.transcribeAudio({
1050
+ audio: uint8Array,
1051
+ language: 'en'
1052
+ });
1053
+
1054
+ // Public URL transcription (for hosted audio files)
1055
+ const { text } = await blink.ai.transcribeAudio({
1056
+ audio: 'https://example.com/audio/meeting.mp3',
1057
+ language: 'en'
1058
+ });
1059
+
1060
+ // Advanced options
1061
+ const { text } = await blink.ai.transcribeAudio({
1062
+ audio: base64Audio,
1063
+ language: 'en',
1064
+ model: 'whisper-1',
1065
+ response_format: 'verbose_json' // Get timestamps and confidence scores
1066
+ });
1067
+
1068
+ // Supported audio formats: MP3, WAV, M4A, FLAC, OGG, WebM
1069
+ // Supported input types:
1070
+ // - string: Base64 data, Data URL (data:audio/...;base64,...), or public URL (https://...)
1071
+ // - ArrayBuffer: Raw audio buffer (works for any file size)
1072
+ // - Uint8Array: Audio data as byte array (works for any file size)
1073
+ // - number[]: Audio data as number array
1074
+
1075
+ // ⚠️ IMPORTANT: For large audio files, use FileReader or ArrayBuffer/Uint8Array
1076
+ // Avoid btoa(String.fromCharCode(...array)) as it crashes with large files
1077
+
1078
+ // Streaming support
1079
+ await blink.ai.streamText(
1080
+ { prompt: 'Write a story...' },
1081
+ (chunk) => console.log(chunk)
1082
+ )
1083
+
1084
+ // Streaming with web search
1085
+ await blink.ai.streamText(
1086
+ { prompt: 'Latest AI news', search: true },
1087
+ (chunk) => console.log(chunk)
1088
+ )
1089
+
1090
+ // React streaming example - parse chunks for immediate UI display
1091
+ const [streamingText, setStreamingText] = useState('')
1092
+
1093
+ await blink.ai.streamText(
1094
+ { prompt: 'Write a story about AI...' },
1095
+ (chunk) => {
1096
+ setStreamingText(prev => prev + chunk) // chunk is a string
1097
+ }
1098
+ )
1099
+ ```
1100
+
1101
+ ### Data Operations
1102
+
1103
+ ```typescript
1104
+ // Simple text extraction (default - returns single string)
1105
+ const text = await blink.data.extractFromUrl('https://example.com/document.pdf');
1106
+ console.log(typeof text); // 'string'
1107
+
1108
+ // Extract with chunking enabled
1109
+ const chunks = await blink.data.extractFromUrl('https://example.com/document.pdf', {
1110
+ chunking: true,
1111
+ chunkSize: 2000
1112
+ });
1113
+ console.log(Array.isArray(chunks)); // true
1114
+
1115
+ // Extract from different file types
1116
+ const csvText = await blink.data.extractFromUrl('https://example.com/data.csv');
1117
+ const htmlText = await blink.data.extractFromUrl('https://example.com/page.html');
1118
+ const jsonText = await blink.data.extractFromUrl('https://example.com/config.json');
1119
+
1120
+ // Extract from uploaded file blob (simple)
1121
+ const fileInput = document.getElementById('fileInput') as HTMLInputElement;
1122
+ const file = fileInput.files[0];
1123
+ const extractedText = await blink.data.extractFromBlob(file);
1124
+
1125
+ // Extract from uploaded file blob (with chunking)
1126
+ const chunks = await blink.data.extractFromBlob(file, {
1127
+ chunking: true,
1128
+ chunkSize: 3000
1129
+ });
1130
+
1131
+ // Website scraping (NEW!) - Crystal clear destructuring
1132
+ const { markdown, metadata, links, extract } = await blink.data.scrape('https://example.com');
1133
+ console.log(markdown); // Clean markdown content
1134
+ console.log(metadata.title); // Page title
1135
+ console.log(links.length); // Number of links found
1136
+
1137
+ // Even cleaner - destructure only what you need
1138
+ const { metadata, extract } = await blink.data.scrape('https://blog.example.com/article');
1139
+ console.log(metadata.title); // Always available
1140
+ console.log(extract.headings); // Always an array
1141
+
1142
+ // Website screenshots (NEW!)
1143
+ const screenshotUrl = await blink.data.screenshot('https://example.com');
1144
+ console.log(screenshotUrl); // Direct URL to screenshot image
1145
+
1146
+ // Full-page screenshot with custom dimensions
1147
+ const fullPageUrl = await blink.data.screenshot('https://example.com', {
1148
+ fullPage: true,
1149
+ width: 1920,
1150
+ height: 1080
1151
+ });
1152
+
1153
+ // 🔥 Web Search (NEW!) - Google search results with clean structure
1154
+ // Perfect for getting real-time information and current data
1155
+
1156
+ // Basic web search - just provide a query
1157
+ const searchResults = await blink.data.search('chatgpt');
1158
+ console.log(searchResults.organic_results); // Main search results
1159
+ console.log(searchResults.related_searches); // Related search suggestions
1160
+ console.log(searchResults.people_also_ask); // People also ask questions
1161
+
1162
+ // Search with location for local results
1163
+ const localResults = await blink.data.search('best restaurants', {
1164
+ location: 'San Francisco,CA,United States'
1165
+ });
1166
+ console.log(localResults.local_results); // Local business results
1167
+ console.log(localResults.organic_results); // Regular web results
1168
+
1169
+ // News search - get latest news articles
1170
+ const newsResults = await blink.data.search('artificial intelligence', {
1171
+ type: 'news'
1172
+ });
1173
+ console.log(newsResults.news_results); // News articles with dates and sources
1174
+
1175
+ // Image search - find images
1176
+ const imageResults = await blink.data.search('elon musk', {
1177
+ type: 'images',
1178
+ limit: 20
1179
+ });
1180
+ console.log(imageResults.image_results); // Image results with thumbnails
1181
+
1182
+ // Search in different languages
1183
+ const spanishResults = await blink.data.search('noticias tecnología', {
1184
+ language: 'es',
1185
+ type: 'news'
1186
+ });
1187
+
1188
+ // Shopping search - find products
1189
+ const shoppingResults = await blink.data.search('macbook pro', {
1190
+ type: 'shopping'
1191
+ });
1192
+ console.log(shoppingResults.shopping_results); // Product results with prices
1193
+
1194
+ // All search types return consistent, structured data:
1195
+ // - organic_results: Main search results (always included)
1196
+ // - related_searches: Related search suggestions
1197
+ // - people_also_ask: FAQ-style questions and answers
1198
+ // - local_results: Local businesses (when location provided)
1199
+ // - news_results: News articles (when type='news')
1200
+ // - image_results: Images (when type='images')
1201
+ // - shopping_results: Products (when type='shopping')
1202
+ // - ads: Sponsored results (when present)
1203
+
1204
+ // 🔥 Secure API Proxy (NEW!) - Make API calls with secret substitution
1205
+
1206
+ // Basic API call with secret substitution
1207
+ const response = await blink.data.fetch({
1208
+ url: 'https://api.sendgrid.com/v3/mail/send',
1209
+ method: 'POST',
1210
+ headers: {
1211
+ 'Authorization': 'Bearer {{sendgrid_api_key}}', // Secret replaced server-side
1212
+ 'Content-Type': 'application/json'
1213
+ },
1214
+ body: {
1215
+ from: { email: 'me@example.com' },
1216
+ personalizations: [{ to: [{ email: 'user@example.com' }] }],
1217
+ subject: 'Hello from Blink',
1218
+ content: [{ type: 'text/plain', value: 'Sent securely through Blink!' }]
1219
+ }
1220
+ });
1221
+
1222
+ console.log('Email sent:', response.status === 200);
1223
+ console.log('Response:', response.body);
1224
+ console.log('Took:', response.durationMs, 'ms');
1225
+
1226
+ // GET request with secret in URL and query params
1227
+ const weatherData = await blink.data.fetch({
1228
+ url: 'https://api.openweathermap.org/data/2.5/weather',
1229
+ method: 'GET',
1230
+ query: {
1231
+ q: 'London',
1232
+ appid: '{{openweather_api_key}}', // Secret replaced in query params
1233
+ units: 'metric'
1234
+ }
1235
+ });
1236
+
1237
+ console.log('Weather:', weatherData.body.main.temp, '°C');
1238
+
1239
+ // Async/background requests (fire-and-forget)
1240
+ const asyncResponse = await blink.data.fetchAsync({
1241
+ url: 'https://api.stripe.com/v1/customers',
1242
+ method: 'POST',
1243
+ headers: {
1244
+ 'Authorization': 'Bearer {{stripe_secret_key}}',
1245
+ 'Content-Type': 'application/x-www-form-urlencoded'
1246
+ },
1247
+ body: 'email=customer@example.com&name=John Doe'
1248
+ });
1249
+
1250
+ console.log(asyncResponse.status); // 'triggered'
1251
+ console.log(asyncResponse.message); // 'Request triggered in background'
1252
+
1253
+ // Multiple secrets in different places
1254
+ const complexRequest = await blink.data.fetch({
1255
+ url: 'https://api.github.com/repos/{{github_username}}/{{repo_name}}/issues',
1256
+ method: 'POST',
1257
+ headers: {
1258
+ 'Authorization': 'token {{github_token}}',
1259
+ 'Accept': 'application/vnd.github.v3+json',
1260
+ 'User-Agent': '{{app_name}}'
1261
+ },
1262
+ body: {
1263
+ title: 'Bug Report',
1264
+ body: 'Found via {{app_name}} monitoring'
1265
+ }
1266
+ });
1267
+
1268
+ // Secret substitution works everywhere:
1269
+ // - URL path: /api/{{version}}/users
1270
+ // - Query params: ?key={{api_key}}&user={{user_id}}
1271
+ // - Headers: Authorization: Bearer {{token}}
1272
+ // - Body: { "apiKey": "{{secret}}", "data": "{{value}}" }
1273
+
1274
+ // Error handling for data extraction
1275
+ try {
1276
+ const result = await blink.data.extractFromUrl('https://example.com/huge-file.pdf');
1277
+ } catch (error) {
1278
+ if (error instanceof BlinkDataError) {
1279
+ console.error('Data processing error:', error.message);
1280
+ }
1281
+ }
1282
+ ```
1283
+
1284
+ ### Storage Operations
1285
+
1286
+ ```typescript
1287
+ // Upload files (returns public URL directly)
1288
+ const { publicUrl } = await blink.storage.upload(
1289
+ file,
1290
+ `uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
1291
+ {
1292
+ upsert: true,
1293
+ onProgress: (percent) => console.log(`${percent}%`)
1294
+ }
1295
+ )
1296
+
1297
+ // ❌ WRONG - Hardcoded extensions break HEIC/PNG/WebP files
1298
+ const wrong = await blink.storage.upload(file, `uploads/${Date.now()}.jpg`) // Corrupts non-JPG files
1299
+ // ✅ CORRECT - Extract file extension
1300
+ const correct = await blink.storage.upload(file, `uploads/${Date.now()}.${file.name.split('.').pop()}`)
1301
+
1302
+ // Remove files
1303
+ await blink.storage.remove('file1.jpg', 'file2.jpg')
1304
+ ```
1305
+
1306
+ ### Notifications Operations
1307
+
1308
+ ```typescript
1309
+ // 🔥 Email Notifications (NEW!) - Send emails with attachments, custom branding, and delivery tracking
1310
+
1311
+ // Send a simple email - returns success status and message ID
1312
+ const result = await blink.notifications.email({
1313
+ to: 'customer@example.com',
1314
+ subject: 'Your order has shipped!',
1315
+ html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
1316
+ })
1317
+
1318
+ console.log(result.success) // true/false - whether email was sent
1319
+ console.log(result.messageId) // "msg_abc123..." - unique message identifier
1320
+
1321
+ // Send with plain text fallback (recommended for better deliverability)
1322
+ const { success, messageId } = await blink.notifications.email({
1323
+ to: 'customer@example.com',
1324
+ subject: 'Welcome to our platform!',
1325
+ html: '<h1>Welcome!</h1><p>Thanks for joining us.</p>',
1326
+ text: 'Welcome!\n\nThanks for joining us.' // Plain text version
1327
+ })
1328
+
1329
+ // Send an email with attachments and custom branding
1330
+ const result = await blink.notifications.email({
1331
+ to: ['team@example.com', 'manager@example.com'],
1332
+ from: 'invoices@mycompany.com', // Must be valid email address
1333
+ replyTo: 'support@mycompany.com',
1334
+ subject: 'New Invoice #12345',
1335
+ html: `
1336
+ <div style="font-family: Arial, sans-serif;">
1337
+ <h2>Invoice Ready</h2>
1338
+ <p>Please find the invoice attached.</p>
1339
+ </div>
1340
+ `,
1341
+ text: 'Invoice Ready\n\nPlease find the invoice attached.',
1342
+ cc: 'accounting@mycompany.com',
1343
+ bcc: 'archive@mycompany.com',
1344
+ attachments: [
1345
+ {
1346
+ url: 'https://mycompany.com/invoices/12345.pdf',
1347
+ filename: 'Invoice-12345.pdf', // Custom filename
1348
+ type: 'application/pdf' // MIME type (optional)
1349
+ },
1350
+ {
1351
+ url: 'https://mycompany.com/terms.pdf',
1352
+ filename: 'Terms-of-Service.pdf'
1353
+ }
1354
+ ]
1355
+ })
1356
+
1357
+ console.log(`Email ${result.success ? 'sent' : 'failed'}`)
1358
+ console.log(`Message ID: ${result.messageId}`)
1359
+
1360
+ // Send to multiple recipients with different recipient types
1361
+ const { success, messageId } = await blink.notifications.email({
1362
+ to: ['customer1@example.com', 'customer2@example.com'],
1363
+ cc: ['manager@example.com'],
1364
+ bcc: ['audit@example.com', 'backup@example.com'],
1365
+ from: 'notifications@mycompany.com',
1366
+ subject: 'Monthly Newsletter',
1367
+ html: '<h2>This Month\'s Updates</h2><p>Here are the highlights...</p>'
1368
+ })
1369
+
1370
+ // Dynamic email content with user data
1371
+ const user = await blink.auth.me()
1372
+ const welcomeEmail = await blink.notifications.email({
1373
+ to: user.email,
1374
+ from: 'welcome@mycompany.com',
1375
+ subject: `Welcome ${user.displayName}!`,
1376
+ html: `
1377
+ <h1>Hi ${user.displayName}!</h1>
1378
+ <p>Welcome to our platform. Your account is now active.</p>
1379
+ <p>Account ID: ${user.id}</p>
1380
+ <a href="https://myapp.com/dashboard">Get Started</a>
1381
+ `,
1382
+ text: `Hi ${user.displayName}!\n\nWelcome to our platform. Your account is now active.\nAccount ID: ${user.id}\n\nGet Started: https://myapp.com/dashboard`
1383
+ })
1384
+
1385
+ // Comprehensive error handling with detailed error information
1386
+ try {
1387
+ const result = await blink.notifications.email({
1388
+ to: 'customer@example.com',
1389
+ subject: 'Important Update',
1390
+ html: '<p>This is an important update about your account.</p>'
1391
+ })
1392
+
1393
+ if (result.success) {
1394
+ console.log('✅ Email sent successfully!')
1395
+ console.log('📧 Message ID:', result.messageId)
1396
+ } else {
1397
+ console.error('❌ Email failed to send')
1398
+ // Handle failed send (retry logic, fallback notification, etc.)
1399
+ }
1400
+
1401
+ } catch (error) {
1402
+ if (error instanceof BlinkNotificationsError) {
1403
+ console.error('❌ Email error:', error.message)
1404
+
1405
+ // Common error scenarios:
1406
+ // - "The 'to', 'subject', and either 'html' or 'text' fields are required."
1407
+ // - "Invalid email address format"
1408
+ // - "Attachment URL must be accessible"
1409
+ // - "Failed to send email: Rate limit exceeded"
1410
+
1411
+ // Handle specific error types
1412
+ if (error.message.includes('Rate limit')) {
1413
+ // Implement retry with backoff
1414
+ console.log('⏳ Rate limited, will retry later')
1415
+ } else if (error.message.includes('Invalid email')) {
1416
+ // Log invalid email for cleanup
1417
+ console.log('📧 Invalid email address, removing from list')
1418
+ }
1419
+ } else {
1420
+ console.error('❌ Unexpected error:', error)
1421
+ }
1422
+ }
1423
+
1424
+ // Email validation and best practices
1425
+ const validateAndSendEmail = async (recipient: string, subject: string, content: string) => {
1426
+ // Basic validation
1427
+ if (!recipient.includes('@') || !subject.trim() || !content.trim()) {
1428
+ throw new Error('Invalid email parameters')
1429
+ }
1430
+
1431
+ try {
1432
+ const result = await blink.notifications.email({
1433
+ to: recipient,
1434
+ from: 'noreply@mycompany.com',
1435
+ subject: subject,
1436
+ html: `
1437
+ <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
1438
+ <div style="background: #f8f9fa; padding: 20px; text-align: center;">
1439
+ <h1 style="color: #333; margin: 0;">My Company</h1>
1440
+ </div>
1441
+ <div style="padding: 20px;">
1442
+ ${content}
1443
+ </div>
1444
+ <div style="background: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #666;">
1445
+ <p>© 2024 My Company. All rights reserved.</p>
1446
+ <p><a href="https://mycompany.com/unsubscribe">Unsubscribe</a></p>
1447
+ </div>
1448
+ </div>
1449
+ `,
1450
+ text: content.replace(/<[^>]*>/g, '') // Strip HTML for text version
1451
+ })
1452
+
1453
+ return result
1454
+ } catch (error) {
1455
+ console.error(`Failed to send email to ${recipient}:`, error)
1456
+ throw error
1457
+ }
1458
+ }
1459
+
1460
+ // Usage with validation
1461
+ try {
1462
+ const result = await validateAndSendEmail(
1463
+ 'customer@example.com',
1464
+ 'Account Verification Required',
1465
+ '<p>Please verify your account by clicking the link below.</p><a href="https://myapp.com/verify">Verify Account</a>'
1466
+ )
1467
+ console.log('Email sent with ID:', result.messageId)
1468
+ } catch (error) {
1469
+ console.error('Email validation or sending failed:', error.message)
1470
+ }
1471
+
1472
+ // Bulk email sending with error handling
1473
+ const sendBulkEmails = async (recipients: string[], subject: string, htmlContent: string) => {
1474
+ const results = []
1475
+
1476
+ for (const recipient of recipients) {
1477
+ try {
1478
+ const result = await blink.notifications.email({
1479
+ to: recipient,
1480
+ from: 'newsletter@mycompany.com',
1481
+ subject,
1482
+ html: htmlContent,
1483
+ text: htmlContent.replace(/<[^>]*>/g, '')
1484
+ })
1485
+
1486
+ results.push({
1487
+ recipient,
1488
+ success: result.success,
1489
+ messageId: result.messageId
1490
+ })
1491
+
1492
+ // Rate limiting: wait between sends
1493
+ await new Promise(resolve => setTimeout(resolve, 100))
1494
+
1495
+ } catch (error) {
1496
+ results.push({
1497
+ recipient,
1498
+ success: false,
1499
+ error: error.message
1500
+ })
1501
+ }
1502
+ }
1503
+
1504
+ return results
1505
+ }
1506
+
1507
+ // Response format details:
1508
+ // ✅ Success response: { success: true, messageId: "msg_abc123...", from: "noreply@project.blink-email.com", to: ["recipient@example.com"], subject: "Email Subject", timestamp: "2024-01-20T10:30:00.000Z" }
1509
+ // ❌ The method throws BlinkNotificationsError on failure
1510
+ // 🔍 Error types: validation errors, rate limits, network issues, invalid attachments
1511
+
1512
+ // API Response Format:
1513
+ // The notifications API returns data directly (not wrapped in {data: ..., error: ...})
1514
+ // This is consistent with other Blink APIs like database and storage
1515
+ // All Blink APIs follow this pattern for clean, predictable responses
1516
+
1517
+ // Best practices:
1518
+ // 1. Always include both HTML and text versions for better deliverability
1519
+ // 2. Use valid email addresses for 'from' field (not display names)
1520
+ // 3. Keep HTML simple with inline CSS for email client compatibility
1521
+ // 4. Handle rate limits with retry logic
1522
+ // 5. Validate email addresses before sending
1523
+ // 6. Use message IDs for tracking and debugging
1524
+ // 7. Include unsubscribe links for compliance
1525
+ ```
1526
+
1527
+ ### Analytics Operations
1528
+
1529
+ ```typescript
1530
+ // 🔥 Analytics (NEW!) - Automatic pageview tracking + custom events
1531
+ // Pageviews are tracked automatically on initialization and route changes
1532
+
1533
+ // Log custom events - context data is added automatically
1534
+ blink.analytics.log('button_clicked', {
1535
+ button_id: 'signup',
1536
+ campaign: 'summer_sale'
1537
+ })
1538
+
1539
+ // All events automatically include:
1540
+ // - timestamp, project_id, user_id, user_email, session_id
1541
+ // - pathname (current page), referrer, screen_width
1542
+ // - device/browser/OS info (parsed server-side)
1543
+ // - channel detection (Organic Search, Social, Direct, etc.)
1544
+ // - UTM parameters (source, medium, campaign, content, term)
1545
+ // - UTM persistence for attribution tracking across sessions
1546
+
1547
+ // Control analytics
1548
+ blink.analytics.disable()
1549
+ blink.analytics.enable()
1550
+ const isEnabled = blink.analytics.isEnabled()
1551
+
1552
+ // Clear attribution data (e.g., when user logs out)
1553
+ blink.analytics.clearAttribution()
1554
+
1555
+ // Features: Privacy-first, offline support, event batching, session management
1556
+ // Attribution: UTM params persist across sessions for conversion tracking
1557
+
1558
+ // How UTM persistence works:
1559
+ // 1. User visits with ?utm_source=google&utm_campaign=summer_sale
1560
+ // 2. These params are saved to localStorage for attribution
1561
+ // 3. Future events (even days later) include these UTM params
1562
+ // 4. Perfect for tracking which campaigns drive conversions
1563
+ // 5. New UTM params override old ones (last-touch model available)
1564
+ ```
1565
+
1566
+ ### Realtime Operations
1567
+
1568
+ **🎉 Zero-Boilerplate Connection Management!**
1569
+ All connection states, queuing, and reconnection are handled automatically. No more "CONNECTING state" errors!
1570
+
1571
+ **⚠️ React Users**: See the [React + Realtime Connections](#react--realtime-connections) section below for proper async cleanup patterns to avoid "Subscription cancelled" errors.
1572
+
1573
+ ```typescript
1574
+ // 🔥 Real-time Messaging & Presence (NEW!)
1575
+ // Perfect for chat apps, live collaboration, multiplayer games, and live updates
1576
+
1577
+ // Simple subscribe and publish (most common pattern)
1578
+ const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
1579
+ console.log('New message:', message.data)
1580
+ console.log('From user:', message.userId)
1581
+ console.log('Message type:', message.type)
1582
+ })
1583
+ // message callback receives RealtimeMessage format:
1584
+ // {
1585
+ // id: '1640995200000-0',
1586
+ // type: 'chat',
1587
+ // data: { text: 'Hello!', timestamp: 1640995200000 },
1588
+ // timestamp: 1640995200000,
1589
+ // userId: 'user123',
1590
+ // metadata: { displayName: 'John' }
1591
+ // }
1592
+
1593
+ // Publish a message to all subscribers - returns message ID
1594
+ const messageId = await blink.realtime.publish('chat-room', 'message', {
1595
+ text: 'Hello everyone!',
1596
+ timestamp: Date.now()
1597
+ })
1598
+ // messageId is string format: '1640995200000-0'
1599
+
1600
+ // Advanced channel usage with presence tracking
1601
+ const channel = blink.realtime.channel('game-lobby')
1602
+
1603
+ // Subscribe with user metadata
1604
+ await channel.subscribe({
1605
+ userId: user.id,
1606
+ metadata: {
1607
+ displayName: user.name,
1608
+ avatar: user.avatar,
1609
+ status: 'online'
1610
+ }
1611
+ })
1612
+
1613
+ // Listen for messages
1614
+ const unsubMessage = channel.onMessage((message) => {
1615
+ if (message.type === 'chat') {
1616
+ addChatMessage(message.data)
1617
+ } else if (message.type === 'game-move') {
1618
+ updateGameState(message.data)
1619
+ }
1620
+ })
1621
+ // message parameter format:
1622
+ // {
1623
+ // id: '1640995200000-0',
1624
+ // type: 'chat',
1625
+ // data: { text: 'Hello!', timestamp: 1640995200000 },
1626
+ // timestamp: 1640995200000,
1627
+ // userId: 'user123',
1628
+ // metadata: { displayName: 'John' }
1629
+ // }
1630
+
1631
+ // Listen for presence changes (who's online)
1632
+ // Callback receives array of PresenceUser objects
1633
+ const unsubPresence = channel.onPresence((users) => {
1634
+ console.log(`${users.length} users online:`)
1635
+ users.forEach(user => {
1636
+ console.log(`- ${user.metadata?.displayName} (${user.userId})`)
1637
+ })
1638
+ updateOnlineUsersList(users)
1639
+ })
1640
+ // users parameter format:
1641
+ // [
1642
+ // {
1643
+ // userId: 'user123',
1644
+ // metadata: { displayName: 'John', status: 'online' },
1645
+ // joinedAt: 1640995200000,
1646
+ // lastSeen: 1640995230000
1647
+ // }
1648
+ // ]
1649
+
1650
+ // Publish different types of messages
1651
+ await channel.publish('chat', { text: 'Hello!' }, { userId: user.id })
1652
+ await channel.publish('game-move', { x: 5, y: 3, piece: 'king' })
1653
+ await channel.publish('typing', { isTyping: true })
1654
+
1655
+ // Get current presence (one-time check)
1656
+ // Returns array of PresenceUser objects directly
1657
+ const currentUsers = await channel.getPresence()
1658
+ console.log('Currently online:', currentUsers.length)
1659
+ // currentUsers is PresenceUser[] format:
1660
+ // [
1661
+ // {
1662
+ // userId: 'user123',
1663
+ // metadata: { displayName: 'John', status: 'online' },
1664
+ // joinedAt: 1640995200000,
1665
+ // lastSeen: 1640995230000
1666
+ // }
1667
+ // ]
1668
+
1669
+ // Get message history - returns array of RealtimeMessage objects
1670
+ const recentMessages = await channel.getMessages({
1671
+ limit: 50,
1672
+ before: lastMessageId // Pagination support
1673
+ })
1674
+ // recentMessages is RealtimeMessage[] format:
1675
+ // [
1676
+ // {
1677
+ // id: '1640995200000-0',
1678
+ // type: 'chat',
1679
+ // data: { text: 'Hello!', timestamp: 1640995200000 },
1680
+ // timestamp: 1640995200000,
1681
+ // userId: 'user123',
1682
+ // metadata: { displayName: 'John' }
1683
+ // }
1684
+ // ]
1685
+
1686
+ // Cleanup when done
1687
+ unsubMessage()
1688
+ unsubPresence()
1689
+ await channel.unsubscribe()
1690
+
1691
+ // Or use the simple unsubscribe from subscribe()
1692
+ unsubscribe()
1693
+
1694
+ // Multiple channels for different features
1695
+ const chatChannel = blink.realtime.channel('chat')
1696
+ const notificationChannel = blink.realtime.channel('notifications')
1697
+ const gameChannel = blink.realtime.channel('game-state')
1698
+
1699
+ // Each channel is independent with its own subscribers and presence
1700
+ await chatChannel.subscribe({ userId: user.id })
1701
+ await notificationChannel.subscribe({ userId: user.id })
1702
+ await gameChannel.subscribe({ userId: user.id, metadata: { team: 'red' } })
1703
+
1704
+ // Real-time collaboration example
1705
+ const docChannel = blink.realtime.channel(`document-${docId}`)
1706
+
1707
+ await docChannel.subscribe({
1708
+ userId: user.id,
1709
+ metadata: {
1710
+ name: user.name,
1711
+ cursor: { line: 1, column: 0 }
1712
+ }
1713
+ })
1714
+
1715
+ // Broadcast cursor movements
1716
+ docChannel.onMessage((message) => {
1717
+ if (message.type === 'cursor-move') {
1718
+ updateUserCursor(message.userId, message.data.position)
1719
+ } else if (message.type === 'text-change') {
1720
+ applyTextChange(message.data.delta)
1721
+ }
1722
+ })
1723
+
1724
+ // Send cursor updates
1725
+ await docChannel.publish('cursor-move', {
1726
+ position: { line: 5, column: 10 }
1727
+ }, { userId: user.id })
1728
+
1729
+ // Send text changes
1730
+ await docChannel.publish('text-change', {
1731
+ delta: { insert: 'Hello', retain: 5 },
1732
+ timestamp: Date.now()
1733
+ })
1734
+
1735
+ // Presence with live cursor positions
1736
+ docChannel.onPresence((users) => {
1737
+ users.forEach(user => {
1738
+ if (user.metadata?.cursor) {
1739
+ showUserCursor(user.userId, user.metadata.cursor)
1740
+ }
1741
+ })
1742
+ })
1743
+
1744
+ // Auto-cleanup on page unload
1745
+ window.addEventListener('beforeunload', () => {
1746
+ docChannel.unsubscribe()
1747
+ })
1748
+
1749
+ // Error handling
1750
+ try {
1751
+ await blink.realtime.publish('restricted-channel', 'message', { data: 'test' })
1752
+ } catch (error) {
1753
+ if (error instanceof BlinkRealtimeError) {
1754
+ console.error('Realtime error:', error.message)
1755
+ }
1756
+ }
1757
+ ```
1758
+
1759
+ ## 🔧 Advanced Usage
1760
+
1761
+ ### Error Handling
1762
+
1763
+ ```typescript
1764
+ import {
1765
+ BlinkAuthError,
1766
+ BlinkAuthErrorCode,
1767
+ BlinkAIError,
1768
+ BlinkStorageError,
1769
+ BlinkDataError,
1770
+ BlinkRealtimeError,
1771
+ BlinkNotificationsError
1772
+ } from '@blinkdotnew/sdk'
1773
+
1774
+ // Authentication error handling
1775
+ try {
1776
+ const user = await blink.auth.signInWithEmail(email, password)
1777
+ } catch (error) {
1778
+ if (error instanceof BlinkAuthError) {
1779
+ switch (error.code) {
1780
+ case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
1781
+ console.log('Email verification required')
1782
+ await blink.auth.sendEmailVerification()
1783
+ break
1784
+ case BlinkAuthErrorCode.INVALID_CREDENTIALS:
1785
+ console.error('Invalid email or password')
1786
+ break
1787
+ case BlinkAuthErrorCode.RATE_LIMITED:
1788
+ console.error('Too many attempts, try again later')
1789
+ break
1790
+ default:
1791
+ console.error('Auth error:', error.message)
1792
+ }
1793
+ }
1794
+ }
1795
+
1796
+ // Other error types
1797
+ try {
1798
+ const { text } = await blink.ai.generateText({ prompt: 'Hello' })
1799
+ } catch (error) {
1800
+ if (error instanceof BlinkAIError) {
1801
+ console.error('AI error:', error.message)
1802
+ }
1803
+ }
1804
+ ```
1805
+
1806
+ ### Custom Configuration
1807
+
1808
+ ```typescript
1809
+ const blink = createClient({
1810
+ projectId: 'your-project',
1811
+ baseUrl: 'https://custom-api.example.com',
1812
+ auth: {
1813
+ mode: 'headless', // 'managed' | 'headless'
1814
+ authUrl: 'https://your-auth-service.com', // Custom auth domain (for all auth endpoints)
1815
+ coreUrl: 'https://custom-core.example.com', // Custom API domain (for db, ai, storage)
1816
+ // Providers controlled via project settings
1817
+ redirectUrl: 'https://myapp.com/dashboard',
1818
+ roles: {
1819
+ admin: { permissions: ['*'] },
1820
+ editor: { permissions: ['posts.create', 'posts.update'], inherit: ['viewer'] },
1821
+ viewer: { permissions: ['posts.read'] }
1822
+ }
1823
+ },
1824
+ httpClient: {
1825
+ timeout: 30000,
1826
+ retries: 3
1827
+ }
1828
+ })
1829
+ ```
1830
+
1831
+ ### TypeScript Support
1832
+
1833
+ The SDK is written in TypeScript and provides full type safety:
1834
+
1835
+ ```typescript
1836
+ interface Todo {
1837
+ id: string
1838
+ title: string
1839
+ isCompleted: boolean // Will be returned as "0" or "1" string from SQLite
1840
+ userId: string // Automatically converted from snake_case user_id
1841
+ createdAt: string // Automatically converted from snake_case created_at
1842
+ }
1843
+
1844
+ // Note: Boolean fields are returned as "0"/"1" strings from SQLite
1845
+ // Use Number(value) > 0 to check boolean values
1846
+ const todos = await blink.db.todos.list<Todo>()
1847
+
1848
+ // Check boolean values properly
1849
+ const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
1850
+ const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)
1851
+
1852
+ // When filtering by boolean values in queries, use "0"/"1" strings
1853
+ const onlyCompleted = await blink.db.todos.list<Todo>({
1854
+ where: { isCompleted: "1" } // Use string "1" for true, "0" for false
1855
+ })
1856
+ // todos is fully typed as Todo[]
1857
+ ```
1858
+
1859
+ ### Secret Management for API Proxy
1860
+
1861
+ The `blink.data.fetch()` method allows you to make secure API calls with automatic secret substitution. Here's how to set it up:
1862
+
1863
+ **Step 1: Store your secrets in your Blink project**
1864
+ Visit your project dashboard at [blink.new](https://blink.new) and add your API keys in the "Secrets" section:
1865
+ - `sendgrid_api_key` → `SG.abc123...`
1866
+ - `openweather_api_key` → `d4f5g6h7...`
1867
+ - `stripe_secret_key` → `sk_live_abc123...`
1868
+
1869
+ **Step 2: Use secrets in your API calls**
1870
+ ```typescript
1871
+ // Secrets are automatically substituted server-side - never exposed to frontend
1872
+ const result = await blink.data.fetch({
1873
+ url: 'https://api.example.com/endpoint',
1874
+ headers: {
1875
+ 'Authorization': 'Bearer {{your_secret_key}}' // Replaced with actual value
1876
+ }
1877
+ })
1878
+ ```
1879
+
1880
+ **Step 3: Secret substitution works everywhere**
1881
+ ```typescript
1882
+ await blink.data.fetch({
1883
+ url: 'https://api.{{service_domain}}/v{{api_version}}/users/{{user_id}}',
1884
+ query: {
1885
+ key: '{{api_key}}',
1886
+ format: 'json'
1887
+ },
1888
+ headers: {
1889
+ 'X-API-Key': '{{secondary_key}}'
1890
+ },
1891
+ body: {
1892
+ token: '{{auth_token}}',
1893
+ data: 'regular string data'
1894
+ }
1895
+ })
1896
+ ```
1897
+
1898
+ All `{{secret_name}}` placeholders are replaced with encrypted values from your project's secret store. Secrets never leave the server and are never visible to your frontend code.
1899
+
1900
+ ## 🌍 Framework Examples
1901
+
1902
+ ### React + Realtime Connections
1903
+
1904
+ **⚠️ Critical: Avoid Multiple WebSocket Connections**
1905
+
1906
+ The most common mistake is using async functions in useEffect that lose the cleanup function:
1907
+
1908
+ ```typescript
1909
+ import type { RealtimeChannel } from '@blinkdotnew/sdk'
1910
+
1911
+ // ❌ WRONG - Async function loses cleanup (causes "Subscription cancelled" errors)
1912
+ useEffect(() => {
1913
+ const initApp = async () => {
1914
+ const channel = blink.realtime.channel('room')
1915
+ await channel.subscribe({ userId: user.id })
1916
+ return () => channel.unsubscribe() // ❌ CLEANUP LOST!
1917
+ }
1918
+ initApp() // Returns Promise, not cleanup function
1919
+ }, [])
1920
+ ```
1921
+
1922
+ ```typescript
1923
+ // ❌ WRONG - Creates new connection on every user change
1924
+ useEffect(() => {
1925
+ const channel = blink.realtime.channel('room')
1926
+ await channel.subscribe({ userId: user.id, metadata: { name: user.name } })
1927
+ return () => channel.unsubscribe()
1928
+ }, [user]) // ❌ Full user object dependency causes reconnections
1929
+ ```
1930
+
1931
+ ```typescript
1932
+ // ✅ CORRECT - Proper async cleanup handling
1933
+ useEffect(() => {
1934
+ if (!user?.id) return
1935
+
1936
+ let channel: RealtimeChannel | null = null
1937
+
1938
+ const initApp = async () => {
1939
+ channel = blink.realtime.channel('room')
1940
+ await channel.subscribe({ userId: user.id })
1941
+ }
1942
+
1943
+ initApp().catch(console.error)
1944
+
1945
+ // Cleanup runs when component unmounts
1946
+ return () => {
1947
+ channel?.unsubscribe()
1948
+ }
1949
+ }, [user?.id]) // ✅ Optional chaining in dependency too
1950
+ ```
1951
+
1952
+ ```typescript
1953
+ // ✅ ALTERNATIVE - Using state for cleanup
1954
+ const [channel, setChannel] = useState<RealtimeChannel | null>(null)
1955
+
1956
+ useEffect(() => {
1957
+ if (!user?.id) return
1958
+
1959
+ const initApp = async () => {
1960
+ const ch = blink.realtime.channel('room')
1961
+ await ch.subscribe({ userId: user.id })
1962
+ setChannel(ch)
1963
+ }
1964
+ initApp().catch(console.error)
1965
+ }, [user?.id])
1966
+
1967
+ useEffect(() => {
1968
+ return () => channel?.unsubscribe()
1969
+ }, [channel])
1970
+ ```
1971
+
1972
+ ```typescript
1973
+ // ✅ COMPLETE EXAMPLE - With proper loading states
1974
+ function MyRealtimeComponent() {
1975
+ const [user, setUser] = useState(null)
1976
+ const [messages, setMessages] = useState([])
1977
+
1978
+ // Auth state management
1979
+ useEffect(() => {
1980
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
1981
+ setUser(state.user)
1982
+ })
1983
+ return unsubscribe
1984
+ }, [])
1985
+
1986
+ // Guard clause - prevent rendering if user not loaded
1987
+ if (!user) return <div>Loading...</div>
1988
+
1989
+ // Now safe to use user.id everywhere
1990
+ useEffect(() => {
1991
+ if (!user?.id) return
1992
+
1993
+ let channel: RealtimeChannel | null = null
1994
+
1995
+ const initApp = async () => {
1996
+ channel = blink.realtime.channel('room')
1997
+ await channel.subscribe({ userId: user.id })
1998
+
1999
+ channel.onMessage((message) => {
2000
+ setMessages(prev => [...prev, message])
2001
+ })
2002
+ }
2003
+
2004
+ initApp().catch(console.error)
2005
+
2006
+ return () => {
2007
+ channel?.unsubscribe()
2008
+ }
2009
+ }, [user?.id])
2010
+
2011
+ return <div>Welcome {user.email}! Messages: {messages.length}</div>
2012
+ }
2013
+ ```
2014
+
2015
+ **Rules:**
2016
+ 1. **Never return cleanup from async functions** - useEffect cleanup must be synchronous
2017
+ 2. **useEffect dependency**: `[user?.id]` not `[user]` to avoid reconnections
2018
+ 3. **Store channel reference** outside async function for cleanup access
2019
+ 4. **Add component-level guards** - Check `if (!user) return <Loading />` before rendering
2020
+ 5. **Zero connection management**: SDK handles all connection states automatically
2021
+
2022
+ ### React
2023
+
2024
+ #### 🔑 Complete Examples by Mode
2025
+
2026
+ **🎯 Managed Mode Example:**
2027
+
2028
+ ```typescript
2029
+ import { createClient } from '@blinkdotnew/sdk'
2030
+
2031
+ const blink = createClient({
2032
+ projectId: 'your-project',
2033
+ auth: { mode: 'managed' }
2034
+ })
2035
+
2036
+ function App() {
2037
+ const [user, setUser] = useState(null)
2038
+
2039
+ useEffect(() => {
2040
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
2041
+ setUser(state.user)
2042
+ })
2043
+ return unsubscribe
2044
+ }, [])
2045
+
2046
+ if (!user) {
2047
+ return (
2048
+ <div>
2049
+ <h1>Welcome to My App</h1>
2050
+ <button onClick={() => blink.auth.login()}>
2051
+ Sign In
2052
+ </button>
2053
+ </div>
2054
+ )
2055
+ }
2056
+
2057
+ return <Dashboard user={user} />
2058
+ }
2059
+ ```
2060
+
2061
+ **🎨 Headless Mode Example:**
2062
+
2063
+ ```typescript
2064
+ import { createClient } from '@blinkdotnew/sdk'
2065
+
2066
+ const blink = createClient({
2067
+ projectId: 'your-project',
2068
+ auth: { mode: 'headless' }
2069
+ })
2070
+
2071
+ function AuthForm() {
2072
+ const [mode, setMode] = useState('signin') // 'signin' | 'signup' | 'reset'
2073
+ const [email, setEmail] = useState('')
2074
+ const [password, setPassword] = useState('')
2075
+ const [message, setMessage] = useState('')
2076
+
2077
+ const handleEmailAuth = async () => {
2078
+ try {
2079
+ if (mode === 'signin') {
2080
+ await blink.auth.signInWithEmail(email, password)
2081
+ } else if (mode === 'signup') {
2082
+ await blink.auth.signUp({ email, password })
2083
+ setMessage('Account created! Check your email to verify.')
2084
+ } else if (mode === 'reset') {
2085
+ await blink.auth.sendPasswordResetEmail(email, {
2086
+ redirectUrl: 'https://myapp.com/reset-password' // Your custom reset page
2087
+ })
2088
+ setMessage('Password reset email sent! Check your inbox.')
2089
+ }
2090
+ } catch (error) {
2091
+ console.error('Auth failed:', error.message)
2092
+ }
2093
+ }
2094
+
2095
+ const handleSocialAuth = async () => {
2096
+ try {
2097
+ await blink.auth.signInWithGoogle()
2098
+ } catch (error) {
2099
+ console.error('Social auth failed:', error.message)
2100
+ }
2101
+ }
2102
+
2103
+ return (
2104
+ <div>
2105
+ {message && <p style={{ color: 'green' }}>{message}</p>}
2106
+
2107
+ <form onSubmit={handleEmailAuth}>
2108
+ <input
2109
+ type="email"
2110
+ value={email}
2111
+ onChange={(e) => setEmail(e.target.value)}
2112
+ placeholder="Email"
2113
+ />
2114
+
2115
+ {mode !== 'reset' && (
2116
+ <input
2117
+ type="password"
2118
+ value={password}
2119
+ onChange={(e) => setPassword(e.target.value)}
2120
+ placeholder="Password"
2121
+ />
2122
+ )}
2123
+
2124
+ <button type="submit">
2125
+ {mode === 'signin' ? 'Sign In' : mode === 'signup' ? 'Sign Up' : 'Send Reset Email'}
2126
+ </button>
2127
+ </form>
2128
+
2129
+ {mode !== 'reset' && (
2130
+ <button type="button" onClick={handleSocialAuth}>
2131
+ Continue with Google
2132
+ </button>
2133
+ )}
2134
+
2135
+ <div>
2136
+ {mode === 'signin' && (
2137
+ <>
2138
+ <button onClick={() => setMode('signup')}>Create Account</button>
2139
+ <button onClick={() => setMode('reset')}>Forgot Password?</button>
2140
+ </>
2141
+ )}
2142
+ {mode === 'signup' && (
2143
+ <button onClick={() => setMode('signin')}>Back to Sign In</button>
2144
+ )}
2145
+ {mode === 'reset' && (
2146
+ <button onClick={() => setMode('signin')}>Back to Sign In</button>
2147
+ )}
2148
+ </div>
2149
+ </div>
2150
+ )
2151
+ }
2152
+ ```
2153
+
2154
+ #### 🔄 Custom Reset Page Handling
2155
+
2156
+ **When users click the reset link, handle it in your app:**
2157
+
2158
+ ```typescript
2159
+ // /reset-password page component
2160
+ function ResetPasswordPage() {
2161
+ const [token, setToken] = useState('')
2162
+ const [projectId, setProjectId] = useState('')
2163
+ const [newPassword, setNewPassword] = useState('')
2164
+ const [message, setMessage] = useState('')
2165
+
2166
+ useEffect(() => {
2167
+ // Extract token and projectId from URL params
2168
+ const params = new URLSearchParams(window.location.search)
2169
+ setToken(params.get('token') || '')
2170
+ setProjectId(params.get('projectId') || '')
2171
+ }, [])
2172
+
2173
+ const handleReset = async (e) => {
2174
+ e.preventDefault()
2175
+
2176
+ try {
2177
+ await blink.auth.confirmPasswordReset(token, newPassword)
2178
+ setMessage('Password reset successfully! You can now sign in.')
2179
+ } catch (error) {
2180
+ console.error('Reset failed:', error.message)
2181
+ }
2182
+ }
2183
+
2184
+ if (!token) return <div>Invalid reset link</div>
2185
+
2186
+ return (
2187
+ <form onSubmit={handleReset}>
2188
+ <h1>Set New Password</h1>
2189
+ <input
2190
+ type="password"
2191
+ value={newPassword}
2192
+ onChange={(e) => setNewPassword(e.target.value)}
2193
+ placeholder="Enter new password"
2194
+ minLength={8}
2195
+ />
2196
+ <button type="submit">Reset Password</button>
2197
+ {message && <p style={{ color: 'green' }}>{message}</p>}
2198
+ </form>
2199
+ )
2200
+ }
2201
+ ```
2202
+
2203
+ #### Custom Email Branding Example
2204
+
2205
+ **Send password reset with your own email service:**
2206
+
2207
+ ```typescript
2208
+ function PasswordResetForm() {
2209
+ const [email, setEmail] = useState('')
2210
+ const [message, setMessage] = useState('')
2211
+
2212
+ const handleReset = async (e) => {
2213
+ e.preventDefault()
2214
+
2215
+ try {
2216
+ // Generate secure token (no email sent by Blink)
2217
+ const resetData = await blink.auth.generatePasswordResetToken(email)
2218
+
2219
+ // Send with your own email service and branding
2220
+ await yourEmailService.send({
2221
+ to: email,
2222
+ from: 'support@yourapp.com',
2223
+ subject: 'Reset your YourApp password',
2224
+ html: `
2225
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 600px; margin: 0 auto;">
2226
+ <div style="text-align: center; padding: 40px;">
2227
+ <img src="https://yourapp.com/logo.svg" alt="YourApp" style="width: 120px; margin-bottom: 32px;" />
2228
+ <h1 style="color: #1a1a1a; font-size: 28px; margin-bottom: 16px;">Reset Your Password</h1>
2229
+ <p style="color: #666; font-size: 16px; margin-bottom: 32px;">
2230
+ We received a request to reset your YourApp password.
2231
+ </p>
2232
+ <a href="${resetData.resetUrl}"
2233
+ style="display: inline-block; background: #0070f3; color: white;
2234
+ padding: 16px 32px; text-decoration: none; border-radius: 8px;
2235
+ font-weight: 600; font-size: 16px;">
2236
+ Reset My Password
2237
+ </a>
2238
+ <p style="color: #999; font-size: 14px; margin-top: 32px;">
2239
+ This link expires in 1 hour. If you didn't request this, you can ignore this email.
2240
+ </p>
2241
+ </div>
2242
+ </div>
2243
+ `
2244
+ })
2245
+
2246
+ setMessage('Password reset email sent! Check your inbox.')
2247
+ } catch (error) {
2248
+ console.error('Reset failed:', error.message)
2249
+ }
2250
+ }
2251
+
2252
+ return (
2253
+ <form onSubmit={handleReset}>
2254
+ <input
2255
+ type="email"
2256
+ value={email}
2257
+ onChange={(e) => setEmail(e.target.value)}
2258
+ placeholder="Enter your email"
2259
+ />
2260
+ <button type="submit">Send Reset Link</button>
2261
+ {message && <p>{message}</p>}
2262
+ </form>
2263
+ )
2264
+ }
2265
+ ```
2266
+
2267
+ **⚠️ Critical: Always Use Auth State Listener, Never One-Time Checks**
2268
+
2269
+ The most common authentication mistake is checking auth status once instead of listening to changes:
2270
+
2271
+ ```typescript
2272
+ // ❌ WRONG - One-time check misses auth completion
2273
+ useEffect(() => {
2274
+ const checkAuth = async () => {
2275
+ try {
2276
+ const userData = await blink.auth.me()
2277
+ setUser(userData)
2278
+ } catch (error) {
2279
+ console.error('Auth check failed:', error)
2280
+ } finally {
2281
+ setLoading(false)
2282
+ }
2283
+ }
2284
+ checkAuth() // Only runs once - misses when auth completes later!
2285
+ }, [])
2286
+
2287
+ // ✅ CORRECT - Listen to auth state changes
2288
+ useEffect(() => {
2289
+ const unsubscribe = blink.auth.onAuthStateChanged((state) => {
2290
+ setUser(state.user)
2291
+ setLoading(state.isLoading)
2292
+ })
2293
+ return unsubscribe
2294
+ }, [])
2295
+ ```
2296
+
2297
+ #### Error Handling Example
2298
+
2299
+ ```typescript
2300
+ import { BlinkAuthError, BlinkAuthErrorCode } from '@blinkdotnew/sdk'
2301
+
2302
+ function AuthForm() {
2303
+ const [error, setError] = useState('')
2304
+
2305
+ const handleAuth = async () => {
2306
+ try {
2307
+ await blink.auth.signInWithEmail(email, password)
2308
+ } catch (err) {
2309
+ if (err instanceof BlinkAuthError) {
2310
+ switch (err.code) {
2311
+ case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
2312
+ setError('Please verify your email first')
2313
+ await blink.auth.sendEmailVerification()
2314
+ break
2315
+ case BlinkAuthErrorCode.INVALID_CREDENTIALS:
2316
+ setError('Invalid email or password')
2317
+ break
2318
+ case BlinkAuthErrorCode.RATE_LIMITED:
2319
+ setError('Too many attempts. Please try again later.')
2320
+ break
2321
+ default:
2322
+ setError('Authentication failed. Please try again.')
2323
+ }
2324
+ }
2325
+ }
2326
+ }
2327
+
2328
+ return (
2329
+ <div>
2330
+ {error && <div style={{ color: 'red' }}>{error}</div>}
2331
+ {/* Auth form */}
2332
+ </div>
2333
+ )
2334
+ }
2335
+ ```
2336
+
2337
+ ```typescript
2338
+ // React example with search functionality
2339
+ function SearchResults() {
2340
+ const [query, setQuery] = useState('')
2341
+ const [results, setResults] = useState(null)
2342
+ const [loading, setLoading] = useState(false)
2343
+
2344
+ const handleSearch = async () => {
2345
+ if (!query.trim()) return
2346
+
2347
+ setLoading(true)
2348
+ try {
2349
+ const searchResults = await blink.data.search(query, {
2350
+ type: 'news', // Get latest news
2351
+ limit: 10
2352
+ })
2353
+ setResults(searchResults)
2354
+ } catch (error) {
2355
+ console.error('Search failed:', error)
2356
+ } finally {
2357
+ setLoading(false)
2358
+ }
2359
+ }
2360
+
2361
+ return (
2362
+ <div>
2363
+ <input
2364
+ value={query}
2365
+ onChange={(e) => setQuery(e.target.value)}
2366
+ placeholder="Search for news..."
2367
+ onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
2368
+ />
2369
+ <button onClick={handleSearch} disabled={loading}>
2370
+ {loading ? 'Searching...' : 'Search'}
2371
+ </button>
2372
+
2373
+ {results && (
2374
+ <div>
2375
+ <h3>News Results:</h3>
2376
+ {results.news_results?.map((article, i) => (
2377
+ <div key={i}>
2378
+ <h4><a href={article.link}>{article.title}</a></h4>
2379
+ <p>{article.snippet}</p>
2380
+ <small>{article.source} - {article.date}</small>
2381
+ </div>
2382
+ ))}
2383
+
2384
+ <h3>Related Searches:</h3>
2385
+ {results.related_searches?.map((suggestion, i) => (
2386
+ <button key={i} onClick={() => setQuery(suggestion)}>
2387
+ {suggestion}
2388
+ </button>
2389
+ ))}
2390
+ </div>
2391
+ )}
2392
+ </div>
2393
+ )
2394
+ }
2395
+
2396
+ // React example with secure API calls
2397
+ function EmailSender() {
2398
+ const [status, setStatus] = useState('')
2399
+
2400
+ const sendEmail = async () => {
2401
+ setStatus('Sending...')
2402
+ try {
2403
+ const response = await blink.data.fetch({
2404
+ url: 'https://api.sendgrid.com/v3/mail/send',
2405
+ method: 'POST',
2406
+ headers: {
2407
+ 'Authorization': 'Bearer {{sendgrid_api_key}}', // Secret safe on server
2408
+ 'Content-Type': 'application/json'
2409
+ },
2410
+ body: {
2411
+ from: { email: 'app@example.com' },
2412
+ personalizations: [{ to: [{ email: user.email }] }],
2413
+ subject: 'Welcome to our app!',
2414
+ content: [{ type: 'text/plain', value: 'Thanks for signing up!' }]
2415
+ }
2416
+ })
2417
+
2418
+ setStatus(response.status === 202 ? 'Email sent!' : 'Failed to send')
2419
+ } catch (error) {
2420
+ setStatus('Error: ' + error.message)
2421
+ }
2422
+ }
2423
+
2424
+ return (
2425
+ <div>
2426
+ <button onClick={sendEmail}>Send Welcome Email</button>
2427
+ <p>{status}</p>
2428
+ </div>
2429
+ )
2430
+ }
2431
+
2432
+ // React example with realtime chat
2433
+ function RealtimeChat() {
2434
+ const [messages, setMessages] = useState([])
2435
+ const [newMessage, setNewMessage] = useState('')
2436
+ const [onlineUsers, setOnlineUsers] = useState([])
2437
+ const [user] = useState({ id: 'user123', name: 'John Doe' }) // From auth
2438
+
2439
+ // Guard clause - prevent rendering if user not loaded
2440
+ if (!user) return <div>Loading...</div>
2441
+
2442
+ const userRef = useRef(user)
2443
+ useEffect(() => { userRef.current = user }, [user])
2444
+
2445
+ useEffect(() => {
2446
+ if (!user?.id) return
2447
+
2448
+ let channel: RealtimeChannel | null = null
2449
+
2450
+ // Subscribe and listen for messages
2451
+ const setupRealtime = async () => {
2452
+ channel = blink.realtime.channel('chat-room')
2453
+ await channel.subscribe({
2454
+ userId: userRef.current.id,
2455
+ metadata: { displayName: userRef.current.name, avatar: '/avatar.png' }
2456
+ })
2457
+
2458
+ // Listen for new messages
2459
+ channel.onMessage((message) => {
2460
+ if (message.type === 'chat') {
2461
+ setMessages(prev => [...prev, {
2462
+ id: message.id,
2463
+ text: message.data.text,
2464
+ userId: message.userId,
2465
+ timestamp: message.timestamp,
2466
+ user: message.metadata?.displayName || 'Unknown'
2467
+ }])
2468
+ }
2469
+ })
2470
+
2471
+ // Listen for presence changes
2472
+ // users is PresenceUser[] with userId, metadata, joinedAt, lastSeen
2473
+ channel.onPresence((users) => {
2474
+ setOnlineUsers(users.map(u => ({
2475
+ id: u.userId,
2476
+ name: u.metadata?.displayName || 'Anonymous',
2477
+ avatar: u.metadata?.avatar
2478
+ })))
2479
+ })
2480
+
2481
+ // Load recent messages - returns RealtimeMessage[] with id, type, data, timestamp, userId, metadata
2482
+ const recentMessages = await channel.getMessages({ limit: 50 })
2483
+ setMessages(recentMessages.map(msg => ({
2484
+ id: msg.id,
2485
+ text: msg.data.text,
2486
+ userId: msg.userId,
2487
+ timestamp: msg.timestamp,
2488
+ user: msg.metadata?.displayName || 'Unknown'
2489
+ })))
2490
+ }
2491
+
2492
+ setupRealtime().catch(console.error)
2493
+
2494
+ // Cleanup on unmount
2495
+ return () => {
2496
+ channel?.unsubscribe()
2497
+ }
2498
+ }, [user?.id]) // ✅ Optional chaining in dependency
2499
+
2500
+ const sendMessage = async () => {
2501
+ if (!newMessage.trim()) return
2502
+
2503
+ try {
2504
+ await blink.realtime.publish('chat-room', 'chat', {
2505
+ text: newMessage,
2506
+ timestamp: Date.now()
2507
+ }, {
2508
+ userId: user.id,
2509
+ metadata: { displayName: user.name }
2510
+ })
2511
+
2512
+ setNewMessage('')
2513
+ } catch (error) {
2514
+ console.error('Failed to send message:', error)
2515
+ }
2516
+ }
2517
+
2518
+ return (
2519
+ <div style={{ display: 'flex', height: '400px' }}>
2520
+ {/* Chat messages */}
2521
+ <div style={{ flex: 1, padding: '1rem' }}>
2522
+ <h3>Chat Room</h3>
2523
+ <div style={{ height: '250px', overflowY: 'auto', border: '1px solid #ccc', padding: '0.5rem' }}>
2524
+ {messages.map((msg) => (
2525
+ <div key={msg.id} style={{ marginBottom: '0.5rem' }}>
2526
+ <strong>{msg.user}:</strong> {msg.text}
2527
+ <small style={{ color: '#666', marginLeft: '0.5rem' }}>
2528
+ {new Date(msg.timestamp).toLocaleTimeString()}
2529
+ </small>
2530
+ </div>
2531
+ ))}
2532
+ </div>
2533
+
2534
+ <div style={{ marginTop: '1rem', display: 'flex' }}>
2535
+ <input
2536
+ value={newMessage}
2537
+ onChange={(e) => setNewMessage(e.target.value)}
2538
+ placeholder="Type a message..."
2539
+ style={{ flex: 1, marginRight: '0.5rem' }}
2540
+ onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
2541
+ />
2542
+ <button onClick={sendMessage}>Send</button>
2543
+ </div>
2544
+ </div>
2545
+
2546
+ {/* Online users sidebar */}
2547
+ <div style={{ width: '200px', borderLeft: '1px solid #ccc', padding: '1rem' }}>
2548
+ <h4>Online ({onlineUsers.length})</h4>
2549
+ {onlineUsers.map((user) => (
2550
+ <div key={user.id} style={{ display: 'flex', alignItems: 'center', marginBottom: '0.5rem' }}>
2551
+ <div style={{
2552
+ width: '8px',
2553
+ height: '8px',
2554
+ backgroundColor: '#22c55e',
2555
+ borderRadius: '50%',
2556
+ marginRight: '0.5rem'
2557
+ }} />
2558
+ {user.name}
2559
+ </div>
2560
+ ))}
2561
+ </div>
2562
+ </div>
2563
+ )
2564
+ }
2565
+ ```
2566
+
2567
+ ### Next.js API Routes
2568
+
2569
+ ```typescript
2570
+ // pages/api/todos.ts
2571
+ import { createClient } from '@blinkdotnew/sdk'
2572
+
2573
+ // ✅ RECOMMENDED: Use new auth configuration
2574
+ const blink = createClient({
2575
+ projectId: 'your-project',
2576
+ auth: { mode: 'managed' } // Explicit configuration
2577
+ })
2578
+
2579
+ export default async function handler(req, res) {
2580
+ const todos = await blink.db.todos.list()
2581
+ res.json(todos)
2582
+ }
2583
+ ```
2584
+
2585
+ ### Deno Edge Function
2586
+
2587
+ ```typescript
2588
+ import { createClient } from '@blinkdotnew/sdk'
2589
+
2590
+ // ✅ RECOMMENDED: Use new auth configuration
2591
+ const blink = createClient({
2592
+ projectId: 'your-project',
2593
+ auth: { mode: 'managed' } // Explicit configuration
2594
+ })
2595
+
2596
+ Deno.serve(async (req) => {
2597
+ const todos = await blink.db.todos.list()
2598
+ return Response.json(todos)
2599
+ })
2600
+ ```
2601
+
2602
+ ## 🔗 Links
2603
+
2604
+ - **🌟 Try Blink AI**: [https://blink.new](https://blink.new) - Build apps in seconds
2605
+ - **📚 Documentation**: [https://docs.blink.new](https://docs.blink.new)
2606
+ - **💻 GitHub**: [https://github.com/ShadowWalker2014/blink-sdk](https://github.com/ShadowWalker2014/blink-sdk)
2607
+
2608
+ ### Social Links
2609
+ - **🐦 X (Twitter)**: [https://x.com/blinkdotnew](https://x.com/blinkdotnew)
2610
+ - **💼 LinkedIn**: [https://www.linkedin.com/company/blinkdotnew](https://www.linkedin.com/company/blinkdotnew)
2611
+ - **💬 Discord**: [https://discord.gg/2RjY7wP4a8](https://discord.gg/2RjY7wP4a8)
2612
+ - **🔴 Reddit**: [https://www.reddit.com/r/blinkdotnew/](https://www.reddit.com/r/blinkdotnew/)
2613
+
2614
+ ## 📄 License
2615
+
2616
+ MIT © Blink Team
2617
+
2618
+ ---
2619
+
2620
+ **Made with ❤️ by the Blink team**
2621
+
2622
+ 🤖 **Ready to build your next app?** Visit [blink.new](https://blink.new) and let our AI create it for you in seconds!