@blinkdotnew/sdk
v2.2.0
Published
Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps
Readme
@blinkdotnew/sdk
The full-stack TypeScript SDK that powers Blink AI-generated apps
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).
🚀 Quick Start
Step 1: Create a Blink Project
Visit blink.new and create a new project. Blink's AI agent will build your app in seconds.
Step 2: Install the SDK
Use Blink's AI agent to automatically install this SDK in your Vite React TypeScript client, or install manually:
npm install @blinkdotnew/sdkStep 3: Use Your Project ID
Get your project ID from your Blink dashboard and start building:
import { createClient } from '@blinkdotnew/sdk'
const blink = createClient({
projectId: 'your-blink-project-id', // From blink.new dashboard
authRequired: false // Don't force immediate auth - let users browse first
})
// Authentication - Choose your mode:
// 🎯 MANAGED MODE: Quick setup with hosted auth page
const blink = createClient({
projectId: 'your-project',
auth: { mode: 'managed' }
})
// Use: blink.auth.login() - redirects to blink.new auth
// 🎨 HEADLESS MODE: Custom UI with full control
const blink = createClient({
projectId: 'your-project',
auth: { mode: 'headless' }
})
// Use: blink.auth.signInWithEmail(), blink.auth.signInWithGoogle(), etc.
// Current user (works in both modes)
const user = await blink.auth.me()
// Database operations (zero config)
const todos = await blink.db.todos.list({
where: { userId: user.id },
orderBy: { createdAt: 'desc' },
limit: 20
})
// AI operations (native)
const { text } = await blink.ai.generateText({
prompt: "Write a summary of the user's todos"
})
// Data operations (extract text from documents)
const text = await blink.data.extractFromUrl("https://example.com/document.pdf")
// Website scraping and screenshots (crystal clear results!)
const { markdown, metadata, links } = await blink.data.scrape("https://competitor.com")
const screenshotUrl = await blink.data.screenshot("https://competitor.com")
// Web search (get real-time information)
const searchResults = await blink.data.search("chatgpt latest news", { type: 'news' })
const localResults = await blink.data.search("best restaurants", { location: "San Francisco,CA,United States" })
// Notifications (NEW!)
const { success } = await blink.notifications.email({
to: '[email protected]',
subject: 'Your order has shipped!',
html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
})
// Secure API proxy (call external APIs with secret substitution)
const response = await blink.data.fetch({
url: "https://api.sendgrid.com/v3/mail/send",
method: "POST",
headers: { "Authorization": "Bearer {{sendgrid_api_key}}" },
body: { /* email data */ }
})
// Realtime operations (live messaging and presence)
const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
console.log('New message:', message.data)
})
await blink.realtime.publish('chat-room', 'message', { text: 'Hello world!' })
// Get presence - returns array of PresenceUser objects directly
const users = await blink.realtime.presence('chat-room')
console.log('Online users:', users.length)
// users is PresenceUser[] format:
// [
// {
// userId: 'user123',
// metadata: { displayName: 'Alice', status: 'online' },
// joinedAt: 1640995200000,
// lastSeen: 1640995230000
// }
// ]
// Analytics operations (automatic pageview tracking + custom events)
// Pageviews are tracked automatically on initialization and route changes
blink.analytics.log('button_clicked', {
button_id: 'signup',
page: '/pricing'
})
// Check if analytics is enabled
if (blink.analytics.isEnabled()) {
console.log('Analytics is active')
}
// Disable/enable analytics
blink.analytics.disable()
blink.analytics.enable()
// Storage operations (instant - returns public URL directly)
const { publicUrl } = await blink.storage.upload(
file,
`avatars/${user.id}-${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
{ upsert: true }
)🤖 What is Blink?
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:
- 🏗️ Generate complete apps with React + TypeScript + Vite
- 🔧 Auto-install this SDK with zero configuration
- 🎨 Create beautiful UIs with Tailwind CSS
- 🚀 Deploy instantly with authentication, database, AI, and storage built-in
📚 SDK Features
This SDK powers every Blink-generated app with:
- 🔐 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
- 🗄️ Database: PostgREST-compatible CRUD operations with advanced filtering
- 🤖 AI: Multi-model image generation & editing (10 models), text generation with web search, object generation, speech synthesis, and transcription
- 📄 Data: Extract text content from documents, secure API proxy with secret substitution, web scraping, screenshots, and web search
- 📁 Storage: File upload, download, and management
- 📧 Notifications: Email sending with attachments, custom branding, and delivery tracking
- ⚡ Realtime: WebSocket-based pub/sub messaging, presence tracking, and live updates
- 📊 Analytics: Automatic pageview tracking, custom event logging, session management, and privacy-first design
- 🌐 Universal: Works on client-side and server-side
- 📱 Framework Agnostic: React, Vue, Svelte, vanilla JS, Node.js, Deno, React Native
- 📱 React Native: First-class mobile support with AsyncStorage integration and platform-aware features
- 🔄 Real-time: Built-in auth state management and token refresh
- ⚡ Zero Boilerplate: Everything works out of the box
🛠️ Manual Installation & Setup
💡 Tip: If you're using Blink's AI agent, this is all done automatically for you!
Client-side (React, Vue, etc.)
import { createClient } from '@blinkdotnew/sdk'
const blink = createClient({
projectId: 'your-blink-project-id', // From blink.new dashboard
authRequired: false, // Let users browse first - require auth only for protected areas
auth: {
mode: 'managed' // Use new explicit configuration
}
})⚠️ 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.
Server-side (Node.js, Deno, Edge functions)
import { createClient } from '@blinkdotnew/sdk'
const blink = createClient({
projectId: 'your-blink-project-id', // From blink.new dashboard
auth: { mode: 'managed' } // Manual token management
})
// Token injection is only needed when calling blink.auth.* methods on the server📱 React Native (iOS & Android)
The SDK has first-class React Native support with platform-aware features that automatically adapt to mobile environments.
Step 1: Install Dependencies
npm install @blinkdotnew/sdk @react-native-async-storage/async-storage expo-web-browserRequired packages:
@blinkdotnew/sdk- The Blink SDK@react-native-async-storage/async-storage- For token persistenceexpo-web-browser- For OAuth authentication (Google, GitHub, Apple, etc.)
Step 2: Create Client (lib/blink.ts)
⚠️ IMPORTANT: You MUST pass webBrowser: WebBrowser to enable OAuth on mobile!
// lib/blink.ts
import { createClient, AsyncStorageAdapter } from '@blinkdotnew/sdk'
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as WebBrowser from 'expo-web-browser' // ← Import this
export const blink = createClient({
projectId: 'your-project-id',
authRequired: false,
auth: {
mode: 'headless',
webBrowser: WebBrowser // ← Pass it here! Required for OAuth
},
storage: new AsyncStorageAdapter(AsyncStorage)
})Step 3: Use Authentication
Email/Password (works immediately):
// Sign up
const user = await blink.auth.signUp({
email: '[email protected]',
password: 'SecurePass123'
})
// Sign in
const user = await blink.auth.signInWithEmail('[email protected]', 'SecurePass123')OAuth (Google, GitHub, Apple, Microsoft):
// ✅ Same code works on web, iOS, AND Android!
const user = await blink.auth.signInWithGoogle()
const user = await blink.auth.signInWithGitHub()
const user = await blink.auth.signInWithApple()
const user = await blink.auth.signInWithMicrosoft()Step 4: Create Auth Hook (hooks/useAuth.ts)
// hooks/useAuth.ts
import { useEffect, useState } from 'react'
import { blink } from '@/lib/blink'
import type { BlinkUser } from '@blinkdotnew/sdk'
export function useAuth() {
const [user, setUser] = useState<BlinkUser | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
setUser(state.user)
setIsLoading(state.isLoading)
})
return unsubscribe
}, [])
return {
user,
isLoading,
isAuthenticated: !!user,
signInWithGoogle: () => blink.auth.signInWithGoogle(),
signInWithGitHub: () => blink.auth.signInWithGitHub(),
signInWithApple: () => blink.auth.signInWithApple(),
signOut: () => blink.auth.signOut(),
}
}Step 5: Use in Components
import { useAuth } from '@/hooks/useAuth'
import { View, Text, Button } from 'react-native'
function App() {
const { user, isLoading, signInWithGoogle, signOut } = useAuth()
if (isLoading) return <Text>Loading...</Text>
if (!user) {
return <Button onPress={signInWithGoogle} title="Sign in with Google" />
}
return (
<View>
<Text>Welcome, {user.email}!</Text>
<Button onPress={signOut} title="Sign Out" />
</View>
)
}Common Mistakes
// ❌ WRONG: Missing webBrowser - OAuth won't work on mobile!
const blink = createClient({
projectId: 'your-project-id',
auth: { mode: 'headless' }, // Missing webBrowser!
storage: new AsyncStorageAdapter(AsyncStorage)
})
// ✅ CORRECT: Include webBrowser for OAuth support
import * as WebBrowser from 'expo-web-browser'
const blink = createClient({
projectId: 'your-project-id',
auth: {
mode: 'headless',
webBrowser: WebBrowser // ← Required for OAuth!
},
storage: new AsyncStorageAdapter(AsyncStorage)
})Low-Level OAuth (For Custom Control)
If you need custom polling timeouts or manual browser handling:
// Get auth URL and authenticate function separately
const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google')
// Open browser manually
await WebBrowser.openAuthSessionAsync(authUrl)
// Poll with custom options
const user = await authenticate({
maxAttempts: 120, // 60 seconds (default: 60 = 30 seconds)
intervalMs: 500 // Check every 500ms
})Platform Features
- ✅ AsyncStorage - Secure token persistence
- ✅ Universal OAuth - Same code works on web + mobile
- ✅ expo-web-browser - Native browser UI
- ✅ No deep linking - Session-based polling
- ✅ Works in Expo Go - No custom dev client needed
- ✅ Auto token refresh - Seamless sessions
📖 API Reference
Authentication
⚠️ 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.
Blink provides two authentication modes:
🎯 Managed Mode (Redirect-based)
Perfect for: Quick setup, minimal code Best for: Websites, simple apps, MVP development
const blink = createClient({
projectId: 'your-project',
auth: { mode: 'managed' }
})
// ONE METHOD: Redirect to hosted auth page
blink.auth.login() // → Redirects to blink.new/auth
blink.auth.logout() // Clear tokens and redirect
// User state (automatic after redirect)
const user = await blink.auth.me()🎨 Headless Mode (Custom UI)
Perfect for: Custom branding, advanced UX, mobile apps Best for: Production apps, branded experiences
const blink = createClient({
projectId: 'your-project',
auth: { mode: 'headless' }
})
// MULTIPLE METHODS: Build your own UI
const user = await blink.auth.signUp({ email, password })
const user = await blink.auth.signInWithEmail(email, password)
const user = await blink.auth.signInWithGoogle()
const user = await blink.auth.signInWithGitHub()
const user = await blink.auth.signInWithApple()
const user = await blink.auth.signInWithMicrosoft()
// ✅ Store custom signup fields inside metadata
await blink.auth.signUp({
email: '[email protected]',
password: 'SuperSecret123',
displayName: 'Alex Founder',
role: 'operations',
metadata: {
company: 'Acme Freight',
marketingConsent: true
}
})
// `displayName`, `avatar`, and `role` map to dedicated auth columns.
// Everything else goes into auth.users.metadata automatically.
// Keep custom fields in metadata or your own profile table—avoid adding NOT NULL
// columns directly to auth tables.
// Magic links (passwordless)
await blink.auth.sendMagicLink(email)
// Password management
await blink.auth.sendPasswordResetEmail(email)
await blink.auth.sendPasswordResetEmail(email, {
redirectUrl: 'https://myapp.com/reset-password'
})
await blink.auth.changePassword(oldPass, newPass)
// Email verification
await blink.auth.sendEmailVerification()⚡ Quick Mode Comparison
| Feature | Managed Mode | Headless Mode |
|---------|------------------|-------------------|
| Setup | 1 line of code | Custom UI required |
| Methods | login() only | signInWith*() methods |
| UI | Hosted auth page | Your custom forms |
| Branding | Blink-branded | Fully customizable |
| Mobile | Web redirects | Native integration |
🚨 Common Mistake
// ❌ WRONG: Using managed method in headless mode
const blink = createClient({
auth: { mode: 'headless' }
})
blink.auth.login() // Still redirects! Wrong method for headless
// ✅ CORRECT: Use headless methods
await blink.auth.signInWithEmail(email, password)
await blink.auth.signInWithGoogle()🔧 Provider Configuration
Step 1: Enable Providers in Your Project
- Go to blink.new and open your project
- Navigate to Project → Workspace → Authentication
- Toggle providers on/off:
- Email ✅ (enabled by default) - Includes email/password, magic links, and verification
- Google ✅ (enabled by default)
- GitHub ⚪ (disabled by default)
- Apple ⚪ (disabled by default)
- Microsoft ⚪ (disabled by default)
- Configure email settings:
- Require email verification: Off by default (easier implementation)
- Allow user signup: On by default
Step 2: Discover Available Providers
// Get providers enabled for your project
const availableProviders = await blink.auth.getAvailableProviders()
// Returns: ['email', 'google'] (based on your project settings)
// Use in your UI to show only enabled providers
const showGoogleButton = availableProviders.includes('google')
const showGitHubButton = availableProviders.includes('github')Step 3: Client-Side Filtering (Headless Mode)
const blink = createClient({
projectId: 'your-project',
auth: {
mode: 'headless'
// All providers controlled via project settings
}
})Managed Mode: Automatic Provider Display
const blink = createClient({
projectId: 'your-project',
auth: { mode: 'managed' }
})
// The hosted auth page automatically shows only enabled providers
blink.auth.login() // Shows Email + Google by default📧 Email Verification Flow
By default, email verification is NOT required for easier implementation. Enable it only if needed:
Step 1: Configure Verification (Optional)
- Go to blink.new → Project → Workspace → Authentication
- Toggle "Require email verification" ON (disabled by default)
Step 2: Handle the Complete Flow
// User signup - always send verification email for security
try {
const user = await blink.auth.signUp({ email, password })
await blink.auth.sendEmailVerification()
setMessage('Account created! Check your email to verify.')
} catch (error) {
setError(error.message)
}
// User signin - handle verification requirement
try {
await blink.auth.signInWithEmail(email, password)
// Success - user is signed in
} catch (error) {
if (error.code === 'EMAIL_NOT_VERIFIED') {
setError('Please verify your email first')
await blink.auth.sendEmailVerification() // Resend verification
} else {
setError(error.message)
}
}
// Manual verification resend
await blink.auth.sendEmailVerification()What Happens:
- Signup: User account created,
email_verified = false - Verification Email: User clicks link →
email_verified = true - Signin Check: If verification required AND not verified →
EMAIL_NOT_VERIFIEDerror - Success: User can sign in once verified (or if verification not required)
Flexible Email System
Maximum flexibility - you control email branding while Blink handles secure tokens:
// Option A: Custom email delivery (full branding control)
const resetData = await blink.auth.generatePasswordResetToken('[email protected]')
// Returns: { token, expiresAt, resetUrl }
// Send with your email service and branding
await yourEmailService.send({
to: '[email protected]',
subject: 'Reset your YourApp password',
html: `
<div style="font-family: Arial, sans-serif;">
<img src="https://yourapp.com/logo.png" alt="YourApp" />
<h1>Reset Your Password</h1>
<a href="${resetData.resetUrl}"
style="background: #0070f3; color: white; padding: 16px 32px;
text-decoration: none; border-radius: 8px;">
Reset Password
</a>
</div>
`
})
// Option B: Blink default email (zero setup)
await blink.auth.sendPasswordResetEmail('[email protected]')
// Same flexibility for email verification and magic links
const verifyData = await blink.auth.generateEmailVerificationToken()
const magicData = await blink.auth.generateMagicLinkToken('[email protected]')Role-Based Access Control (RBAC)
// Configure roles and permissions
const blink = createClient({
projectId: 'your-project',
auth: {
mode: 'headless',
roles: {
admin: { permissions: ['*'] },
editor: { permissions: ['posts.create', 'posts.update'] },
viewer: { permissions: ['posts.read'] }
}
}
})
// Check permissions
const canEdit = blink.auth.can('posts.update')
const isAdmin = blink.auth.hasRole('admin')
const isStaff = blink.auth.hasRole(['admin', 'editor'])
// Use in components
function EditButton() {
if (!blink.auth.can('posts.update')) return null
return <button onClick={editPost}>Edit Post</button>
}Core Methods
⚠️ 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.
// User management
const user = await blink.auth.me()
await blink.auth.updateMe({ displayName: 'New Name' })
// Token management
blink.auth.setToken(jwt, persist?)
const isAuth = blink.auth.isAuthenticated()
const token = await blink.auth.getValidToken() // Get valid token (auto-refreshes)
// Password management
await blink.auth.sendPasswordResetEmail('[email protected]')
await blink.auth.sendPasswordResetEmail('[email protected]', {
redirectUrl: 'https://myapp.com/reset-password' // Custom reset page
})
await blink.auth.changePassword('oldPass', 'newPass')
await blink.auth.confirmPasswordReset(token, newPassword)
// Email verification
await blink.auth.verifyEmail(token)
// Provider discovery
const providers = await blink.auth.getAvailableProviders()
// Auth state listener (REQUIRED for React apps!)
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
console.log('Auth state:', state)
// state.user - current user or null
// state.isLoading - true while auth is initializing
// state.isAuthenticated - true if user is logged in
// state.tokens - current auth tokens
})Login Redirect Behavior
When login() is called, the SDK automatically determines where to redirect after authentication:
// Automatic redirect (uses current page URL)
blink.auth.login()
// → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/current-page
// Custom redirect URL
blink.auth.login('https://yourapp.com/dashboard')
// → Redirects to: blink.new/auth?redirect_url=https://yourapp.com/dashboard
// Manual login button example
const handleLogin = () => {
// The SDK will automatically use the current page URL
blink.auth.login()
// Or specify a custom redirect
// blink.auth.login('https://yourapp.com/welcome')
}✅ Fixed in v1.x: The SDK now ensures redirect URLs are always absolute, preventing broken redirects when window.location.href returns relative paths.
Database Operations
🎉 NEW: Automatic Case Conversion!
The SDK now automatically converts between JavaScript camelCase and SQL snake_case:
- Table names:
blink.db.emailDrafts→email_draftstable - Field names:
userId,createdAt,isCompleted→user_id,created_at,is_completed - No manual conversion needed!
⚠️ Important: Always Use camelCase in Your Code
- ✅ Correct:
blink.db.emailDrafts.create({ userId: user.id, createdAt: new Date() }) - ❌ Wrong:
blink.db.email_drafts.create({ user_id: user.id, created_at: new Date() })
// Create (ID auto-generated if not provided)
const todo = await blink.db.todos.create({
id: 'todo_12345', // Optional - auto-generated if not provided
title: 'Learn Blink SDK',
userId: user.id, // camelCase in code
createdAt: new Date(), // camelCase in code
isCompleted: false // camelCase in code
})
// Read with filtering - returns camelCase fields
const todos = await blink.db.todos.list({
where: {
AND: [
{ userId: user.id }, // camelCase in filters
{ OR: [{ status: 'open' }, { priority: 'high' }] }
]
},
orderBy: { createdAt: 'desc' }, // camelCase in orderBy
limit: 20
})
// `todos` is a direct array: Todo[]
// Note: Boolean fields are returned as "0"/"1" strings from SQLite
// Check boolean values using Number(value) > 0
const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)
// Update
await blink.db.todos.update(todo.id, { isCompleted: true })
// Delete
await blink.db.todos.delete(todo.id)
// Bulk operations (IDs auto-generated if not provided)
await blink.db.todos.createMany([
{ title: 'Task 1', userId: user.id }, // ID will be auto-generated
{ id: 'custom_id', title: 'Task 2', userId: user.id } // Custom ID provided
])
await blink.db.todos.upsertMany([...])
AI Operations
// Text generation (simple prompt)
const { text } = await blink.ai.generateText({
prompt: 'Write a poem about coding',
maxTokens: 150
})
// Web search (OpenAI only) - get real-time information
const { text, sources } = await blink.ai.generateText({
prompt: 'Who is the current US president?',
search: true // Returns current info + source URLs
})
// Multi-step reasoning - for complex analysis
const { text } = await blink.ai.generateText({
prompt: 'Research and analyze tech trends',
search: true,
maxSteps: 10, // Override default (25 when tools used)
experimental_continueSteps: true // Override default (true when tools used)
})
// Text generation with image content
// ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
// For file uploads, use blink.storage.upload() first to get public HTTPS URLs
// 🚫 CRITICAL: When uploading files, NEVER hardcode extensions - use file.name or auto-detection
const { text } = await blink.ai.generateText({
messages: [
{
role: "user",
content: [
{ type: "text", text: "What do you see in this image?" },
{ type: "image", image: "https://storage.googleapis.com/.../.../photo.jpg" }
]
}
]
})
// Mixed content with multiple images
const { text } = await blink.ai.generateText({
messages: [
{
role: "user",
content: [
{ type: "text", text: "Compare these two images:" },
{ type: "image", image: "https://storage.googleapis.com/.../.../image1.jpg" },
{ type: "image", image: "https://cdn.example.com/image2.png" }
]
}
]
})
// Structured object generation
const { object } = await blink.ai.generateObject({
prompt: 'Generate a user profile',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' }
}
}
})
// ⚠️ IMPORTANT: Schema Rule for generateObject()
// The top-level schema MUST use type: "object" - you cannot use type: "array" at the top level
// This ensures clear, robust, and extensible API calls with named parameters
// ✅ Correct: Array inside object
const { object: todoList } = await blink.ai.generateObject({
prompt: 'Generate a list of 5 daily tasks',
schema: {
type: 'object',
properties: {
tasks: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string' },
priority: { type: 'string', enum: ['low', 'medium', 'high'] }
}
}
}
},
required: ['tasks']
}
})
// Result: { tasks: [{ title: "Exercise", priority: "high" }, ...] }
// ❌ Wrong: Top-level array (will fail)
// const { object } = await blink.ai.generateObject({
// prompt: 'Generate tasks',
// schema: {
// type: 'array', // ❌ This will throw an error
// items: { type: 'string' }
// }
// })
// Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"
// Generate and modify images with AI - Multi-Model Support (10 models available)
// 🔥 Choose between fast generation or high-quality results
// 🎨 For style transfer: provide ALL images in the images array, don't reference URLs in prompts
// Basic image generation (uses default fast model: fal-ai/nano-banana)
const { data } = await blink.ai.generateImage({
prompt: 'A serene landscape with mountains and a lake at sunset'
})
console.log('Image URL:', data[0].url)
// High-quality generation with Pro model
const { data: proImage } = await blink.ai.generateImage({
prompt: 'A detailed infographic about AI with charts and diagrams',
model: 'fal-ai/nano-banana-pro', // High quality model
n: 1,
size: '1792x1024' // Custom size
})
// Generate multiple variations
const { data } = await blink.ai.generateImage({
prompt: 'A futuristic robot in different poses',
model: 'fal-ai/nano-banana', // Fast model
n: 3
})
data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url))
**Available Models for Text-to-Image:**
| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `fal-ai/nano-banana` (default) | ⚡ Fast | Good | Prototypes, high-volume generation |
| `fal-ai/nano-banana-pro` | Standard | ⭐ Excellent | Marketing materials, high-fidelity visuals |
| `fal-ai/gemini-25-flash-image` | ⚡ Fast | Good | Alias for `nano-banana` |
| `fal-ai/gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Alias for `nano-banana-pro` |
| `gemini-2.5-flash-image-preview` | ⚡ Fast | Good | Legacy - Direct Gemini API |
| `gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Legacy - Direct Gemini API |
// Image editing - transform existing images with prompts (uses default fast model)
const { data: headshots } = await blink.ai.modifyImage({
images: ['https://storage.example.com/user-photo.jpg'], // Up to 16 images supported!
prompt: 'Transform into professional business headshot with studio lighting'
})
// High-quality editing with Pro model
const { data: proEdited } = await blink.ai.modifyImage({
images: ['https://storage.example.com/portrait.jpg'],
prompt: 'Add a majestic ancient tree in the background with glowing leaves',
model: 'fal-ai/nano-banana-pro/edit' // High quality editing
})
// Advanced image editing with multiple input images
const { data } = await blink.ai.modifyImage({
images: [
'https://storage.example.com/photo1.jpg',
'https://storage.example.com/photo2.jpg',
'https://storage.example.com/photo3.jpg'
],
prompt: 'Combine these architectural styles into a futuristic building design',
model: 'fal-ai/nano-banana/edit', // Fast editing
n: 2
})
**Available Models for Image Editing:**
| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `fal-ai/nano-banana/edit` (default) | ⚡ Fast | Good | Quick adjustments, style transfers |
| `fal-ai/nano-banana-pro/edit` | Standard | ⭐ Excellent | Detailed retouching, complex edits |
| `fal-ai/gemini-25-flash-image/edit` | ⚡ Fast | Good | Alias for `nano-banana/edit` |
| `fal-ai/gemini-3-pro-image-preview/edit` | Standard | ⭐ Excellent | Alias for `nano-banana-pro/edit` |
// 🎨 Style Transfer & Feature Application
// ⚠️ IMPORTANT: When applying styles/features from one image to another,
// provide ALL images in the array - don't reference images in the prompt text
// ❌ WRONG - Don't reference images in prompt
const wrongWay = await blink.ai.modifyImage({
images: [userPhotoUrl],
prompt: `Apply this hairstyle from the reference image to the person's head. Reference hairstyle: ${hairstyleUrl}`
})
// ✅ CORRECT - Provide all images in the array
const { data } = await blink.ai.modifyImage({
images: [userPhotoUrl, hairstyleUrl], // Both images provided
prompt: 'Apply the hairstyle from the second image to the person in the first image. Blend naturally with face shape.'
})
// More style transfer examples
const { data } = await blink.ai.modifyImage({
images: [portraitUrl, artworkUrl],
prompt: 'Apply the artistic style and color palette from the second image to the portrait in the first image'
})
const { data } = await blink.ai.modifyImage({
images: [roomPhotoUrl, designReferenceUrl],
prompt: 'Redesign the room in the first image using the interior design style shown in the second image'
})
// Multiple reference images for complex transformations
const { data } = await blink.ai.modifyImage({
images: [
originalPhotoUrl,
lightingReferenceUrl,
colorPaletteReferenceUrl,
compositionReferenceUrl
],
prompt: 'Transform the first image using the lighting style from image 2, color palette from image 3, and composition from image 4'
})
// 📱 File Upload + Style Transfer
// ⚠️ Extract file extension properly - never hardcode .jpg/.png
// ❌ WRONG - Hardcoded extension
const userUpload = await blink.storage.upload(file, `photos/${Date.now()}.jpg`) // Breaks HEIC/PNG files
// ✅ CORRECT - Extract original extension
const userUpload = await blink.storage.upload(
userPhoto.file,
`photos/${Date.now()}.${userPhoto.file.name.split('.').pop()}`
)
const hairstyleUpload = await blink.storage.upload(
hairstylePhoto.file,
`haircuts/${Date.now()}.${hairstylePhoto.file.name.split('.').pop()}`
)
const { data } = await blink.ai.modifyImage({
images: [userUpload.publicUrl, hairstyleUpload.publicUrl],
prompt: 'Apply hairstyle from second image to person in first image'
})
// Speech synthesis
const { url } = await blink.ai.generateSpeech({
text: 'Hello, world!',
voice: 'nova'
})
// Audio transcription - Multiple input formats supported
// 🔥 Most common: Browser audio recording → Base64 → Transcription
let mediaRecorder: MediaRecorder;
let audioChunks: Blob[] = [];
// Step 1: Start recording
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
audioChunks.push(event.data);
};
mediaRecorder.start();
// Step 2: Stop recording and transcribe
mediaRecorder.onstop = async () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
// SAFE method for large files - use FileReader (recommended)
const base64 = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result as string;
const base64Data = dataUrl.split(',')[1]; // Extract base64 part
resolve(base64Data);
};
reader.onerror = reject;
reader.readAsDataURL(audioBlob);
});
// Transcribe using base64 (preferred method)
const { text } = await blink.ai.transcribeAudio({
audio: base64, // Raw base64 string
language: 'en'
});
console.log('Transcription:', text);
audioChunks = []; // Reset for next recording
};
// Alternative: Data URL format (also supported)
const reader = new FileReader();
reader.onload = async () => {
const dataUrl = reader.result as string;
const { text } = await blink.ai.transcribeAudio({
audio: dataUrl, // Data URL format
language: 'en'
});
};
reader.readAsDataURL(audioBlob);
// File upload transcription
const fileInput = document.getElementById('audioFile') as HTMLInputElement;
const file = fileInput.files[0];
// Option 1: Convert to base64 using FileReader (recommended for large files)
const base64Audio = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result as string;
const base64Data = dataUrl.split(',')[1]; // Extract base64 part
resolve(base64Data);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
const { text } = await blink.ai.transcribeAudio({
audio: base64Audio,
language: 'en'
});
// Option 2: Use ArrayBuffer directly (works for any file size)
const arrayBuffer = await file.arrayBuffer();
const { text } = await blink.ai.transcribeAudio({
audio: arrayBuffer,
language: 'en'
});
// Option 3: Use Uint8Array directly (works for any file size)
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const { text } = await blink.ai.transcribeAudio({
audio: uint8Array,
language: 'en'
});
// Public URL transcription (for hosted audio files)
const { text } = await blink.ai.transcribeAudio({
audio: 'https://example.com/audio/meeting.mp3',
language: 'en'
});
// Advanced options
const { text } = await blink.ai.transcribeAudio({
audio: base64Audio,
language: 'en',
model: 'whisper-1',
response_format: 'verbose_json' // Get timestamps and confidence scores
});
// Supported audio formats: MP3, WAV, M4A, FLAC, OGG, WebM
// Supported input types:
// - string: Base64 data, Data URL (data:audio/...;base64,...), or public URL (https://...)
// - ArrayBuffer: Raw audio buffer (works for any file size)
// - Uint8Array: Audio data as byte array (works for any file size)
// - number[]: Audio data as number array
// ⚠️ IMPORTANT: For large audio files, use FileReader or ArrayBuffer/Uint8Array
// Avoid btoa(String.fromCharCode(...array)) as it crashes with large files
// Streaming support
await blink.ai.streamText(
{ prompt: 'Write a story...' },
(chunk) => console.log(chunk)
)
// Streaming with web search
await blink.ai.streamText(
{ prompt: 'Latest AI news', search: true },
(chunk) => console.log(chunk)
)
// React streaming example - parse chunks for immediate UI display
const [streamingText, setStreamingText] = useState('')
await blink.ai.streamText(
{ prompt: 'Write a story about AI...' },
(chunk) => {
setStreamingText(prev => prev + chunk) // chunk is a string
}
)Data Operations
// Simple text extraction (default - returns single string)
const text = await blink.data.extractFromUrl('https://example.com/document.pdf');
console.log(typeof text); // 'string'
// Extract with chunking enabled
const chunks = await blink.data.extractFromUrl('https://example.com/document.pdf', {
chunking: true,
chunkSize: 2000
});
console.log(Array.isArray(chunks)); // true
// Extract from different file types
const csvText = await blink.data.extractFromUrl('https://example.com/data.csv');
const htmlText = await blink.data.extractFromUrl('https://example.com/page.html');
const jsonText = await blink.data.extractFromUrl('https://example.com/config.json');
// Extract from uploaded file blob (simple)
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const file = fileInput.files[0];
const extractedText = await blink.data.extractFromBlob(file);
// Extract from uploaded file blob (with chunking)
const chunks = await blink.data.extractFromBlob(file, {
chunking: true,
chunkSize: 3000
});
// Website scraping (NEW!) - Crystal clear destructuring
const { markdown, metadata, links, extract } = await blink.data.scrape('https://example.com');
console.log(markdown); // Clean markdown content
console.log(metadata.title); // Page title
console.log(links.length); // Number of links found
// Even cleaner - destructure only what you need
const { metadata, extract } = await blink.data.scrape('https://blog.example.com/article');
console.log(metadata.title); // Always available
console.log(extract.headings); // Always an array
// Website screenshots (NEW!)
const screenshotUrl = await blink.data.screenshot('https://example.com');
console.log(screenshotUrl); // Direct URL to screenshot image
// Full-page screenshot with custom dimensions
const fullPageUrl = await blink.data.screenshot('https://example.com', {
fullPage: true,
width: 1920,
height: 1080
});
// 🔥 Web Search (NEW!) - Google search results with clean structure
// Perfect for getting real-time information and current data
// Basic web search - just provide a query
const searchResults = await blink.data.search('chatgpt');
console.log(searchResults.organic_results); // Main search results
console.log(searchResults.related_searches); // Related search suggestions
console.log(searchResults.people_also_ask); // People also ask questions
// Search with location for local results
const localResults = await blink.data.search('best restaurants', {
location: 'San Francisco,CA,United States'
});
console.log(localResults.local_results); // Local business results
console.log(localResults.organic_results); // Regular web results
// News search - get latest news articles
const newsResults = await blink.data.search('artificial intelligence', {
type: 'news'
});
console.log(newsResults.news_results); // News articles with dates and sources
// Image search - find images
const imageResults = await blink.data.search('elon musk', {
type: 'images',
limit: 20
});
console.log(imageResults.image_results); // Image results with thumbnails
// Search in different languages
const spanishResults = await blink.data.search('noticias tecnología', {
language: 'es',
type: 'news'
});
// Shopping search - find products
const shoppingResults = await blink.data.search('macbook pro', {
type: 'shopping'
});
console.log(shoppingResults.shopping_results); // Product results with prices
// All search types return consistent, structured data:
// - organic_results: Main search results (always included)
// - related_searches: Related search suggestions
// - people_also_ask: FAQ-style questions and answers
// - local_results: Local businesses (when location provided)
// - news_results: News articles (when type='news')
// - image_results: Images (when type='images')
// - shopping_results: Products (when type='shopping')
// - ads: Sponsored results (when present)
// 🔥 Secure API Proxy (NEW!) - Make API calls with secret substitution
// Basic API call with secret substitution
const response = await blink.data.fetch({
url: 'https://api.sendgrid.com/v3/mail/send',
method: 'POST',
headers: {
'Authorization': 'Bearer {{sendgrid_api_key}}', // Secret replaced server-side
'Content-Type': 'application/json'
},
body: {
from: { email: '[email protected]' },
personalizations: [{ to: [{ email: '[email protected]' }] }],
subject: 'Hello from Blink',
content: [{ type: 'text/plain', value: 'Sent securely through Blink!' }]
}
});
console.log('Email sent:', response.status === 200);
console.log('Response:', response.body);
console.log('Took:', response.durationMs, 'ms');
// GET request with secret in URL and query params
const weatherData = await blink.data.fetch({
url: 'https://api.openweathermap.org/data/2.5/weather',
method: 'GET',
query: {
q: 'London',
appid: '{{openweather_api_key}}', // Secret replaced in query params
units: 'metric'
}
});
console.log('Weather:', weatherData.body.main.temp, '°C');
// Async/background requests (fire-and-forget)
const asyncResponse = await blink.data.fetchAsync({
url: 'https://api.stripe.com/v1/customers',
method: 'POST',
headers: {
'Authorization': 'Bearer {{stripe_secret_key}}',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: '[email protected]&name=John Doe'
});
console.log(asyncResponse.status); // 'triggered'
console.log(asyncResponse.message); // 'Request triggered in background'
// Multiple secrets in different places
const complexRequest = await blink.data.fetch({
url: 'https://api.github.com/repos/{{github_username}}/{{repo_name}}/issues',
method: 'POST',
headers: {
'Authorization': 'token {{github_token}}',
'Accept': 'application/vnd.github.v3+json',
'User-Agent': '{{app_name}}'
},
body: {
title: 'Bug Report',
body: 'Found via {{app_name}} monitoring'
}
});
// Secret substitution works everywhere:
// - URL path: /api/{{version}}/users
// - Query params: ?key={{api_key}}&user={{user_id}}
// - Headers: Authorization: Bearer {{token}}
// - Body: { "apiKey": "{{secret}}", "data": "{{value}}" }
// Error handling for data extraction
try {
const result = await blink.data.extractFromUrl('https://example.com/huge-file.pdf');
} catch (error) {
if (error instanceof BlinkDataError) {
console.error('Data processing error:', error.message);
}
}Storage Operations
// Upload files (returns public URL directly)
const { publicUrl } = await blink.storage.upload(
file,
`uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
{
upsert: true,
onProgress: (percent) => console.log(`${percent}%`)
}
)
// ❌ WRONG - Hardcoded extensions break HEIC/PNG/WebP files
const wrong = await blink.storage.upload(file, `uploads/${Date.now()}.jpg`) // Corrupts non-JPG files
// ✅ CORRECT - Extract file extension
const correct = await blink.storage.upload(file, `uploads/${Date.now()}.${file.name.split('.').pop()}`)
// Remove files
await blink.storage.remove('file1.jpg', 'file2.jpg')Notifications Operations
// 🔥 Email Notifications (NEW!) - Send emails with attachments, custom branding, and delivery tracking
// Send a simple email - returns success status and message ID
const result = await blink.notifications.email({
to: '[email protected]',
subject: 'Your order has shipped!',
html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
})
console.log(result.success) // true/false - whether email was sent
console.log(result.messageId) // "msg_abc123..." - unique message identifier
// Send with plain text fallback (recommended for better deliverability)
const { success, messageId } = await blink.notifications.email({
to: '[email protected]',
subject: 'Welcome to our platform!',
html: '<h1>Welcome!</h1><p>Thanks for joining us.</p>',
text: 'Welcome!\n\nThanks for joining us.' // Plain text version
})
// Send an email with attachments and custom branding
const result = await blink.notifications.email({
to: ['[email protected]', '[email protected]'],
from: '[email protected]', // Must be valid email address
replyTo: '[email protected]',
subject: 'New Invoice #12345',
html: `
<div style="font-family: Arial, sans-serif;">
<h2>Invoice Ready</h2>
<p>Please find the invoice attached.</p>
</div>
`,
text: 'Invoice Ready\n\nPlease find the invoice attached.',
cc: '[email protected]',
bcc: '[email protected]',
attachments: [
{
url: 'https://mycompany.com/invoices/12345.pdf',
filename: 'Invoice-12345.pdf', // Custom filename
type: 'application/pdf' // MIME type (optional)
},
{
url: 'https://mycompany.com/terms.pdf',
filename: 'Terms-of-Service.pdf'
}
]
})
console.log(`Email ${result.success ? 'sent' : 'failed'}`)
console.log(`Message ID: ${result.messageId}`)
// Send to multiple recipients with different recipient types
const { success, messageId } = await blink.notifications.email({
to: ['[email protected]', '[email protected]'],
cc: ['[email protected]'],
bcc: ['[email protected]', '[email protected]'],
from: '[email protected]',
subject: 'Monthly Newsletter',
html: '<h2>This Month\'s Updates</h2><p>Here are the highlights...</p>'
})
// Dynamic email content with user data
const user = await blink.auth.me()
const welcomeEmail = await blink.notifications.email({
to: user.email,
from: '[email protected]',
subject: `Welcome ${user.displayName}!`,
html: `
<h1>Hi ${user.displayName}!</h1>
<p>Welcome to our platform. Your account is now active.</p>
<p>Account ID: ${user.id}</p>
<a href="https://myapp.com/dashboard">Get Started</a>
`,
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`
})
// Comprehensive error handling with detailed error information
try {
const result = await blink.notifications.email({
to: '[email protected]',
subject: 'Important Update',
html: '<p>This is an important update about your account.</p>'
})
if (result.success) {
console.log('✅ Email sent successfully!')
console.log('📧 Message ID:', result.messageId)
} else {
console.error('❌ Email failed to send')
// Handle failed send (retry logic, fallback notification, etc.)
}
} catch (error) {
if (error instanceof BlinkNotificationsError) {
console.error('❌ Email error:', error.message)
// Common error scenarios:
// - "The 'to', 'subject', and either 'html' or 'text' fields are required."
// - "Invalid email address format"
// - "Attachment URL must be accessible"
// - "Failed to send email: Rate limit exceeded"
// Handle specific error types
if (error.message.includes('Rate limit')) {
// Implement retry with backoff
console.log('⏳ Rate limited, will retry later')
} else if (error.message.includes('Invalid email')) {
// Log invalid email for cleanup
console.log('📧 Invalid email address, removing from list')
}
} else {
console.error('❌ Unexpected error:', error)
}
}
// Email validation and best practices
const validateAndSendEmail = async (recipient: string, subject: string, content: string) => {
// Basic validation
if (!recipient.includes('@') || !subject.trim() || !content.trim()) {
throw new Error('Invalid email parameters')
}
try {
const result = await blink.notifications.email({
to: recipient,
from: '[email protected]',
subject: subject,
html: `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<div style="background: #f8f9fa; padding: 20px; text-align: center;">
<h1 style="color: #333; margin: 0;">My Company</h1>
</div>
<div style="padding: 20px;">
${content}
</div>
<div style="background: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #666;">
<p>© 2024 My Company. All rights reserved.</p>
<p><a href="https://mycompany.com/unsubscribe">Unsubscribe</a></p>
</div>
</div>
`,
text: content.replace(/<[^>]*>/g, '') // Strip HTML for text version
})
return result
} catch (error) {
console.error(`Failed to send email to ${recipient}:`, error)
throw error
}
}
// Usage with validation
try {
const result = await validateAndSendEmail(
'[email protected]',
'Account Verification Required',
'<p>Please verify your account by clicking the link below.</p><a href="https://myapp.com/verify">Verify Account</a>'
)
console.log('Email sent with ID:', result.messageId)
} catch (error) {
console.error('Email validation or sending failed:', error.message)
}
// Bulk email sending with error handling
const sendBulkEmails = async (recipients: string[], subject: string, htmlContent: string) => {
const results = []
for (const recipient of recipients) {
try {
const result = await blink.notifications.email({
to: recipient,
from: '[email protected]',
subject,
html: htmlContent,
text: htmlContent.replace(/<[^>]*>/g, '')
})
results.push({
recipient,
success: result.success,
messageId: result.messageId
})
// Rate limiting: wait between sends
await new Promise(resolve => setTimeout(resolve, 100))
} catch (error) {
results.push({
recipient,
success: false,
error: error.message
})
}
}
return results
}
// Response format details:
// ✅ Success response: { success: true, messageId: "msg_abc123...", from: "[email protected]", to: ["[email protected]"], subject: "Email Subject", timestamp: "2024-01-20T10:30:00.000Z" }
// ❌ The method throws BlinkNotificationsError on failure
// 🔍 Error types: validation errors, rate limits, network issues, invalid attachments
// API Response Format:
// The notifications API returns data directly (not wrapped in {data: ..., error: ...})
// This is consistent with other Blink APIs like database and storage
// All Blink APIs follow this pattern for clean, predictable responses
// Best practices:
// 1. Always include both HTML and text versions for better deliverability
// 2. Use valid email addresses for 'from' field (not display names)
// 3. Keep HTML simple with inline CSS for email client compatibility
// 4. Handle rate limits with retry logic
// 5. Validate email addresses before sending
// 6. Use message IDs for tracking and debugging
// 7. Include unsubscribe links for complianceAnalytics Operations
// 🔥 Analytics (NEW!) - Automatic pageview tracking + custom events
// Pageviews are tracked automatically on initialization and route changes
// Log custom events - context data is added automatically
blink.analytics.log('button_clicked', {
button_id: 'signup',
campaign: 'summer_sale'
})
// All events automatically include:
// - timestamp, project_id, user_id, user_email, session_id
// - pathname (current page), referrer, screen_width
// - device/browser/OS info (parsed server-side)
// - channel detection (Organic Search, Social, Direct, etc.)
// - UTM parameters (source, medium, campaign, content, term)
// - UTM persistence for attribution tracking across sessions
// Control analytics
blink.analytics.disable()
blink.analytics.enable()
const isEnabled = blink.analytics.isEnabled()
// Clear attribution data (e.g., when user logs out)
blink.analytics.clearAttribution()
// Features: Privacy-first, offline support, event batching, session management
// Attribution: UTM params persist across sessions for conversion tracking
// How UTM persistence works:
// 1. User visits with ?utm_source=google&utm_campaign=summer_sale
// 2. These params are saved to localStorage for attribution
// 3. Future events (even days later) include these UTM params
// 4. Perfect for tracking which campaigns drive conversions
// 5. New UTM params override old ones (last-touch model available)Realtime Operations
🎉 Zero-Boilerplate Connection Management!
All connection states, queuing, and reconnection are handled automatically. No more "CONNECTING state" errors!
⚠️ React Users: See the React + Realtime Connections section below for proper async cleanup patterns to avoid "Subscription cancelled" errors.
// 🔥 Real-time Messaging & Presence (NEW!)
// Perfect for chat apps, live collaboration, multiplayer games, and live updates
// Simple subscribe and publish (most common pattern)
const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
console.log('New message:', message.data)
console.log('From user:', message.userId)
console.log('Message type:', message.type)
})
// message callback receives RealtimeMessage format:
// {
// id: '1640995200000-0',
// type: 'chat',
// data: { text: 'Hello!', timestamp: 1640995200000 },
// timestamp: 1640995200000,
// userId: 'user123',
// metadata: { displayName: 'John' }
// }
// Publish a message to all subscribers - returns message ID
const messageId = await blink.realtime.publish('chat-room', 'message', {
text: 'Hello everyone!',
timestamp: Date.now()
})
// messageId is string format: '1640995200000-0'
// Advanced channel usage with presence tracking
const channel = blink.realtime.channel('game-lobby')
// Subscribe with user metadata
await channel.subscribe({
userId: user.id,
metadata: {
displayName: user.name,
avatar: user.avatar,
status: 'online'
}
})
// Listen for messages
const unsubMessage = channel.onMessage((message) => {
if (message.type === 'chat') {
addChatMessage(message.data)
} else if (message.type === 'game-move') {
updateGameState(message.data)
}
})
// message parameter format:
// {
// id: '1640995200000-0',
// type: 'chat',
// data: { text: 'Hello!', timestamp: 1640995200000 },
// timestamp: 1640995200000,
// userId: 'user123',
// metadata: { displayName: 'John' }
// }
// Listen for presence changes (who's online)
// Callback receives array of PresenceUser objects
const unsubPresence = channel.onPresence((users) => {
console.log(`${users.length} users online:`)
users.forEach(user => {
console.log(`- ${user.metadata?.displayName} (${user.userId})`)
})
updateOnlineUsersList(users)
})
// users parameter format:
// [
// {
// userId: 'user123',
// metadata: { displayName: 'John', status: 'online' },
// joinedAt: 1640995200000,
// lastSeen: 1640995230000
// }
// ]
// Publish different types of messages
await channel.publish('chat', { text: 'Hello!' }, { userId: user.id })
await channel.publish('game-move', { x: 5, y: 3, piece: 'king' })
await channel.publish('typing', { isTyping: true })
// Get current presence (one-time check)
// Returns array of PresenceUser objects directly
const currentUsers = await channel.getPresence()
console.log('Currently online:', currentUsers.length)
// currentUsers is PresenceUser[] format:
// [
// {
// userId: 'user123',
// metadata: { displayName: 'John', status: 'online' },
// joinedAt: 1640995200000,
// lastSeen: 1640995230000
// }
// ]
// Get message history - returns array of RealtimeMessage objects
const recentMessages = await channel.getMessages({
limit: 50,
before: lastMessageId // Pagination support
})
// recentMessages is RealtimeMessage[] format:
// [
// {
// id: '1640995200000-0',
// type: 'chat',
// data: { text: 'Hello!', timestamp: 1640995200000 },
// timestamp: 1640995200000,
// userId: 'user123',
// metadata: { displayName: 'John' }
// }
// ]
// Cleanup when done
unsubMessage()
unsubPresence()
await channel.unsubscribe()
// Or use the simple unsubscribe from subscribe()
unsubscribe()
// Multiple channels for different features
const chatChannel = blink.realtime.channel('chat')
const notificationChannel = blink.realtime.channel('notifications')
const gameChannel = blink.realtime.channel('game-state')
// Each channel is independent with its own subscribers and presence
await chatChannel.subscribe({ userId: user.id })
await notificationChannel.subscribe({ userId: user.id })
await gameChannel.subscribe({ userId: user.id, metadata: { team: 'red' } })
// Real-time collaboration example
const docChannel = blink.realtime.channel(`document-${docId}`)
await docChannel.subscribe({
userId: user.id,
metadata: {
name: user.name,
cursor: { line: 1, column: 0 }
}
})
// Broadcast cursor movements
docChannel.onMessage((message) => {
if (message.type === 'cursor-move') {
updateUserCursor(message.userId, message.data.position)
} else if (message.type === 'text-change') {
applyTextChange(message.data.delta)
}
})
// Send cursor updates
await docChannel.publish('cursor-move', {
position: { line: 5, column: 10 }
}, { userId: user.id })
// Send text changes
await docChannel.publish('text-change', {
delta: { insert: 'Hello', retain: 5 },
timestamp: Date.now()
})
// Presence with live cursor positions
docChannel.onPresence((users) => {
users.forEach(user => {
if (user.metadata?.cursor) {
showUserCursor(user.userId, user.metadata.cursor)
}
})
})
// Auto-cleanup on page unload
window.addEventListener('beforeunload', () => {
docChannel.unsubscribe()
})
// Error handling
try {
await blink.realtime.publish('restricted-channel', 'message', { data: 'test' })
} catch (error) {
if (error instanceof BlinkRealtimeError) {
console.error('Realtime error:', error.message)
}
}🔧 Advanced Usage
Error Handling
import {
BlinkAuthError,
BlinkAuthErrorCode,
BlinkAIError,
BlinkStorageError,
BlinkDataError,
BlinkRealtimeError,
BlinkNotificationsError
} from '@blinkdotnew/sdk'
// Authentication error handling
try {
const user = await blink.auth.signInWithEmail(email, password)
} catch (error) {
if (error instanceof BlinkAuthError) {
switch (error.code) {
case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
console.log('Email verification required')
await blink.auth.sendEmailVerification()
break
case BlinkAuthErrorCode.INVALID_CREDENTIALS:
console.error('Invalid email or password')
break
case BlinkAuthErrorCode.RATE_LIMITED:
console.error('Too many attempts, try again later')
break
default:
console.error('Auth error:', error.message)
}
}
}
// Other error types
try {
const { text } = await blink.ai.generateText({ prompt: 'Hello' })
} catch (error) {
if (error instanceof BlinkAIError) {
console.error('AI error:', error.message)
}
}Custom Configuration
const blink = createClient({
projectId: 'your-project',
baseUrl: 'https://custom-api.example.com',
auth: {
mode: 'headless', // 'managed' | 'headless'
authUrl: 'https://your-auth-service.com', // Custom auth domain (for all auth endpoints)
coreUrl: 'https://custom-core.example.com', // Custom API domain (for db, ai, storage)
// Providers controlled via project settings
redirectUrl: 'https://myapp.com/dashboard',
roles: {
admin: { permissions: ['*'] },
editor: { permissions: ['posts.create', 'posts.update'], inherit: ['viewer'] },
viewer: { permissions: ['posts.read'] }
}
},
httpClient: {
timeout: 30000,
retries: 3
}
})TypeScript Support
The SDK is written in TypeScript and provides full type safety:
interface Todo {
id: string
title: string
isCompleted: boolean // Will be returned as "0" or "1" string from SQLite
userId: string // Automatically converted from snake_case user_id
createdAt: string // Automatically converted from snake_case created_at
}
// Note: Boolean fields are returned as "0"/"1" strings from SQLite
// Use Number(value) > 0 to check boolean values
const todos = await blink.db.todos.list<Todo>()
// Check boolean values properly
const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)
// When filtering by boolean values in queries, use "0"/"1" strings
const onlyCompleted = await blink.db.todos.list<Todo>({
where: { isCompleted: "1" } // Use string "1" for true, "0" for false
})
// todos is fully typed as Todo[]Secret Management for API Proxy
The blink.data.fetch() method allows you to make secure API calls with automatic secret substitution. Here's how to set it up:
Step 1: Store your secrets in your Blink project Visit your project dashboard at blink.new and add your API keys in the "Secrets" section:
sendgrid_api_key→SG.abc123...openweather_api_key→d4f5g6h7...stripe_secret_key→sk_live_abc123...
Step 2: Use secrets in your API calls
// Secrets are automatically substituted server-side - never exposed to frontend
const result = await blink.data.fetch({
url: 'https://api.example.com/endpoint',
headers: {
'Authorization': 'Bearer {{your_secret_key}}' // Replaced with actual value
}
})Step 3: Secret substitution works everywhere
await blink.data.fetch({
url: 'https://api.{{service_domain}}/v{{api_version}}/users/{{user_id}}',
query: {
key: '{{api_key}}',
format: 'json'
},
headers: {
'X-API-Key': '{{secondary_key}}'
},
body: {
token: '{{auth_token}}',
data: 'regular string data'
}
})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.
🌍 Framework Examples
React + Realtime Connections
⚠️ Critical: Avoid Multiple WebSocket Connections
The most common mistake is using async functions in useEffect that lose the cleanup function:
import type { RealtimeChannel } from '@blinkdotnew/sdk'
// ❌ WRONG - Async function loses cleanup (causes "Subscription cancelled" errors)
useEffect(() => {
const initApp = async () => {
const channel = blink.realtime.channel('room')
await channel.subscribe({ userId: user.id })
return () => channel.unsubscribe() // ❌ CLEANUP LOST!
}
initApp() // Returns Promise, not cleanup function
}, [])// ❌ WRONG - Creates new connection on every user change
useEffect(() => {
const channel = blink.realtime.channel('room')
await channel.subscribe({ userId: user.id, metadata: { name: user.name } })
return () => channel.unsubscribe()
}, [user]) // ❌ Full user object dependency causes reconnections// ✅ CORRECT - Proper async cleanup handling
useEffect(() => {
if (!user?.id) return
let channel: RealtimeChannel | null = null
const initApp = async () => {
channel = blink.realtime.channel('room')
await channel.subscribe({ userId: user.id })
}
initApp().catch(console.error)
// Cleanup runs when component unmounts
return () => {
channel?.unsubscribe()
}
}, [user?.id]) // ✅ Optional chaining in dependency too// ✅ ALTERNATIVE - Using state for cleanup
const [channel, setChannel] = useState<RealtimeChannel | null>(null)
useEffect(() => {
if (!user?.id) return
const initApp = async () => {
const ch = blink.realtime.channel('room')
await ch.subscribe({ userId: user.id })
setChannel(ch)
}
initApp().catch(console.error)
}, [user?.id])
useEffect(() => {
return () => channel?.unsubscribe()
}, [channel])// ✅ COMPLETE EXAMPLE - With proper loading states
function MyRealtimeComponent() {
const [user, setUser] = useState(null)
const [messages, setMessages] = useState([])
// Auth state management
useEffect(() => {
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
setUser(state.user)
})
return unsubscribe
}, [])
// Guard clause - prevent rendering if user not loaded
if (!user) return <div>Loading...</div>
// Now safe to use user.id everywhere
useEffect(() => {
if (!user?.id) return
let channel: RealtimeChannel | null = null
const initApp = async () => {
channel = blink.realtime.channel('room')
await channel.subscribe({ userId: user.id })
channel.onMessage((message) => {
setMessages(prev => [...prev, message])
})
}
initApp().catch(console.error)
return () => {
channel?.unsubscribe()
}
}, [user?.id])
return <div>Welcome {user.email}! Messages: {messages.length}</div>
}Rules:
- **Never return cle
