UNPKG

@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
# @blinkdotnew/sdk [![npm version](https://badge.fury.io/js/%40blinkdotnew%2Fsdk.svg)](https://badge.fury.io/js/%40blinkdotnew%2Fsdk) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](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