@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/LICENSE +21 -0
- package/README.md +2622 -0
- package/dist/index.d.mts +2365 -0
- package/dist/index.d.ts +2365 -0
- package/dist/index.js +5556 -0
- package/dist/index.mjs +5536 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,2622 @@
|
|
|
1
|
+
# @blinkdotnew/sdk
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/%40blinkdotnew%2Fsdk)
|
|
4
|
+
[](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!
|