@blinkdotnew/sdk
Version:
Blink TypeScript SDK for client-side applications - Zero-boilerplate CRUD + auth + AI + analytics + notifications for modern SaaS/AI apps
1,583 lines (1,319 loc) β’ 56.9 kB
Markdown
# @blinkdotnew/sdk
[](https://badge.fury.io/js/%40blinkdotnew%2Fsdk)
[](https://www.typescriptlang.org/)
**The full-stack TypeScript SDK that powers Blink AI-generated apps**
Blink is an AI App Developer 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](https://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:
```bash
npm install @blinkdotnew/sdk
```
### **Step 3: Use Your Project ID**
Get your project ID from your Blink dashboard and start building:
```typescript
import { createClient } from '@blinkdotnew/sdk'
const blink = createClient({
projectId: 'your-blink-project-id', // From blink.new dashboard
authRequired: true
})
// Authentication (built-in)
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: 'customer@example.com',
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}.png`,
{ upsert: true }
)
```
## π€ What is Blink?
**Blink is an AI App Developer** 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**: JWT-based auth with automatic token management
- **ποΈ Database**: PostgREST-compatible CRUD operations with advanced filtering
- **π€ AI**: Text generation with web search, object generation, image creation, 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
- **π 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.)
```typescript
import { createClient } from '@blinkdotnew/sdk'
const blink = createClient({
projectId: 'your-blink-project-id', // From blink.new dashboard
authRequired: true // Automatic auth redirect
})
```
### Server-side (Node.js, Deno, Edge functions)
```typescript
import { createClient } from '@blinkdotnew/sdk'
const blink = createClient({
projectId: 'your-blink-project-id', // From blink.new dashboard
authRequired: false // Manual token management
})
// Set JWT manually
blink.auth.setToken(jwtFromHeader)
```
## π API Reference
### Authentication
```typescript
// Login/logout
blink.auth.login(nextUrl?) // Redirect to auth page
blink.auth.logout(redirectUrl?) // Clear tokens and redirect
// 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()
// 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:
```typescript
// 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_drafts` table
- **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() })`
```typescript
// 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
```typescript
// Text generation (simple prompt)
const { text } = await blink.ai.generateText({
prompt: 'Write a poem about coding',
model: 'gpt-4o-mini',
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
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
// Basic image generation - returns public URLs directly
const { data } = await blink.ai.generateImage({
prompt: 'A serene landscape with mountains and a lake at sunset',
size: '1024x1024',
quality: 'high',
n: 2
})
console.log('Image URL:', data[0].url)
// Image editing - transform existing images with prompts
const { data: headshots } = await blink.ai.modifyImage({
images: ['https://storage.example.com/user-photo.jpg'], // ... up to 16 images maximum!
prompt: 'Generate professional business headshot with studio lighting for this person.',
quality: 'high',
n: 4
})
// Options: size ('1024x1024', '1536x1024'), quality ('auto'|'low'|'medium'|'high'),
// background ('auto'|'transparent'|'opaque'), n (1-10 images)
// 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
```typescript
// 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: 'me@example.com' },
personalizations: [{ to: [{ email: 'user@example.com' }] }],
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=customer@example.com&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
```typescript
// Upload files (returns public URL directly)
const { publicUrl } = await blink.storage.upload(
file,
'path/to/file',
{
upsert: true,
onProgress: (percent) => console.log(`${percent}%`)
}
)
// If file is PNG, final path will be: path/to/file.png
// π‘ Pro tip: Use the actual filename (file.name) in your path to avoid confusion
const { publicUrl } = await blink.storage.upload(
file,
`uploads/${file.name}`, // Uses actual filename with correct extension
{ upsert: true }
)
// Remove files
await blink.storage.remove('file1.jpg', 'file2.jpg')
```
### Notifications Operations
```typescript
// π₯ 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: 'customer@example.com',
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: 'customer@example.com',
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: ['team@example.com', 'manager@example.com'],
from: 'invoices@mycompany.com', // Must be valid email address
replyTo: 'support@mycompany.com',
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: 'accounting@mycompany.com',
bcc: 'archive@mycompany.com',
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: ['customer1@example.com', 'customer2@example.com'],
cc: ['manager@example.com'],
bcc: ['audit@example.com', 'backup@example.com'],
from: 'notifications@mycompany.com',
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: 'welcome@mycompany.com',
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: 'customer@example.com',
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: 'noreply@mycompany.com',
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(
'customer@example.com',
'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: 'newsletter@mycompany.com',
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: "noreply@project.blink-email.com", to: ["recipient@example.com"], 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 compliance
```
### Analytics Operations
```typescript
// π₯ 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](#react--realtime-connections) section below for proper async cleanup patterns to avoid "Subscription cancelled" errors.
```typescript
// π₯ 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
```typescript
import { BlinkAuthError, BlinkAIError, BlinkStorageError, BlinkDataError, BlinkRealtimeError, BlinkNotificationsError } from '@blinkdotnew/sdk'
try {
const user = await blink.auth.me()
} catch (error) {
if (error instanceof BlinkAuthError) {
// Handle auth errors
console.error('Auth error:', error.message)
}
}
```
### Custom Configuration
```typescript
const blink = createClient({
projectId: 'your-project',
baseUrl: 'https://custom-api.example.com',
authRequired: true,
httpClient: {
timeout: 30000,
retries: 3
}
})
```
### TypeScript Support
The SDK is written in TypeScript and provides full type safety:
```typescript
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](https://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**
```typescript
// 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**
```typescript
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:
```typescript
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
}, [])
```
```typescript
// β 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
```
```typescript
// β
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
```
```typescript
// β
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])
```
```typescript
// β
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:**
1. **Never return cleanup from async functions** - useEffect cleanup must be synchronous
2. **useEffect dependency**: `[user?.id]` not `[user]` to avoid reconnections
3. **Store channel reference** outside async function for cleanup access
4. **Add component-level guards** - Check `if (!user) return <Loading />` before rendering
5. **Zero connection management**: SDK handles all connection states automatically
### React
**β οΈ Critical: Always Use Auth State Listener, Never One-Time Checks**
The most common authentication mistake is checking auth status once instead of listening to changes:
```typescript
// β WRONG - One-time check misses auth completion
useEffect(() => {
const checkAuth = async () => {
try {
const userData = await blink.auth.me()
setUser(userData)
} catch (error) {
console.error('Auth check failed:', error)
} finally {
setLoading(false)
}
}
checkAuth() // Only runs once - misses when auth completes later!
}, [])
// β
CORRECT - Listen to auth state changes
useEffect(() => {
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
setUser(state.user)
setLoading(state.isLoading)
})
return unsubscribe
}, [])
```
**Why the one-time check fails:**
1. App loads β `blink.auth.me()` called immediately
2. Auth still initializing β Call fails, user set to `null`
3. Auth completes later β App never knows because it only checked once
4. User stuck on "Please sign in" screen forever
**Always use `onAuthStateChanged()` for React apps!**
```typescript
import { createClient } from '@blinkdotnew/sdk'
import { useState, useEffect } from 'react'
const blink = createClient({ projectId: 'your-project', authRequired: true })
function App() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
setUser(state.user)
setLoading(state.isLoading)
})
return unsubscribe
}, [])
if (loading) return <div>Loading...</div>
if (!user) return <div>Please log in</div>
return <div>Welcome, {user.email}!</div>
}
// β WRONG - One-time auth check (misses auth state changes)
function BadApp() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const checkAuth = async () => {
try {
const userData = await blink.auth.me()
setUser(userData)
} catch (error) {
console.error('Auth check failed:', error)
} finally {
setLoading(false)
}
}
checkAuth() // β Only runs once - misses when auth completes later!
}, [])
// This pattern causes users to get stuck on "Please sign in"
// even after authentication completes
}
// React example with search functionality
function SearchResults() {
const [query, setQuery] = useState('')
const [results, setResults] = useState(null)
const [loading, setLoading] = useState(false)
const handleSearch = async () => {
if (!query.trim()) return
setLoading(true)
try {
const searchResults = await blink.data.search(query, {
type: 'news', // Get latest news
limit: 10
})
setResults(searchResults)
} catch (error) {
console.error('Search failed:', error)
} finally {
setLoading(false)
}
}
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for news..."
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
/>
<button onClick={handleSearch} disabled={loading}>
{loading ? 'Searching...' : 'Search'}
</button>
{results && (
<div>
<h3>News Results:</h3>
{results.news_results?.map((article, i) => (
<div key={i}>
<h4><a href={article.link}>{article.title}</a></h4>
<p>{article.snippet}</p>
<small>{article.source} - {article.date}</small>
</div>
))}
<h3>Related Searches:</h3>
{results.related_searches?.map((suggestion, i) => (
<button key={i} onClick={() => setQuery(suggestion)}>
{suggestion}
</button>
))}
</div>
)}
</div>
)
}
// React example with secure API calls
function EmailSender() {
const [status, setStatus] = useState('')
const sendEmail = async () => {
setStatus('Sending...')
try {
const response = await blink.data.fetch({
url: 'https