@blinkdotnew/sdk

中文文档
2.11.4发布于 2026/11/04MITTypeScriptESM + CJS零依赖近 30 天下载 1.3万

用于客户端应用程序的 Blink TypeScript SDK - 0-boilerplate CRUD + auth + AI + 分析 + 现代 SaaS/AI 应用程序的通知

中文使用文档
文档目录30 节
  1. QQ 快速启动
  2. 步骤1: 创建一个闪烁工程
  3. 第2步:安装SDK
  4. 第3步: 使用您的工程ID
  5. 什么是闪烁?
  6. SDK 特性
  7. QQ 手动安装和设置
  8. 客户端( 反应, Vue等)
  9. 服务器侧 (Node.js, Deno, Edge 函数)
  10. QQ 本地反应( iOS & Android)
  11. API 参考
  12. 认证
  13. QQ 管理模式( 直接模式)
  14. QQ 无头模式( Custom UI)
  15. QQ 快速模式比较
  16. 🚨 常见错误
  17. 数据库操作
  18. AI 操作
  19. 数据操作
  20. 存储操作
  21. 通知业务
  22. 分析操作
  23. 实时操作
  24. 高级用途
  25. 处理错误
  26. 自定义配置
  27. 类型脚本支持
  28. API 代理服务器的秘密管理
  29. 框架实例
  30. 反应 + 实时连接

@ blinkdotnew/ sdk (英语)

npm version TypeScript

授权 Blink AI 生成的应用程序的全存储 TypeScript SDK

Blink是一款AI App Builder,以秒为单位构建出功能完备的应用程序. 这个SDK( S)@blinkdotnew/sdk)是TypeScript的基础,它赋予每个 Blink app 本地权限,提供0-boilerplate认证,数据库操作,AI能力,以及文件存储. 在客户端(React,Vue等)和服务器端(Node.js,Deno,Edge功能)上无缝工作.

QQ 快速启动

访问 眨一眼,换一眼 并创建一个新项目。 布莱克的AI代理 将在几秒钟内构建你的应用程序.

第2步:安装SDK

使用 Blink 的 AI 代理在您的 Vite React TypeScript 客户端中自动安装此 SDK, 或者手动安装 :

npm install @blinkdotnew/sdk

第3步: 使用您的工程ID

从您的闪烁仪表板上获取您的项目ID并开始构建:

import { createClient } from '@blinkdotnew/sdk'

const blink = createClient({
  projectId: 'your-blink-project-id', // From blink.new dashboard
  authRequired: false // Don't force immediate auth - let users browse first
})

// Authentication - Choose your mode:

// 🎯 MANAGED MODE: Quick setup with hosted auth page
const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'managed' }
})
// Use: blink.auth.login() - redirects to blink.new auth

// 🎨 HEADLESS MODE: Custom UI with full control  
const blink = createClient({
  projectId: 'your-project', 
  auth: { mode: 'headless' }
})
// Use: blink.auth.signInWithEmail(), blink.auth.signInWithGoogle(), etc.

// Current user (works in both modes)
const user = await blink.auth.me()

// Database operations (zero config)
const todos = await blink.db.todos.list({
  where: { userId: user.id },
  orderBy: { createdAt: 'desc' },
  limit: 20
})

// AI operations (native)
const { text } = await blink.ai.generateText({
  prompt: "Write a summary of the user's todos"
})

// Data operations (extract text from documents)
const text = await blink.data.extractFromUrl("https://example.com/document.pdf")

// Website scraping and screenshots (crystal clear results!)
const { markdown, metadata, links } = await blink.data.scrape("https://competitor.com")
const screenshotUrl = await blink.data.screenshot("https://competitor.com")

// Web search (get real-time information)
const searchResults = await blink.data.search("chatgpt latest news", { type: 'news' })
const localResults = await blink.data.search("best restaurants", { location: "San Francisco,CA,United States" })

// Notifications (NEW!)
const { success } = await blink.notifications.email({
  to: '[email protected]',
  subject: 'Your order has shipped!',
  html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
})

// Secure API proxy (call external APIs with secret substitution)
const response = await blink.data.fetch({
  url: "https://api.sendgrid.com/v3/mail/send",
  method: "POST",
  headers: { "Authorization": "Bearer {{sendgrid_api_key}}" },
  body: { /* email data */ }
})

// Realtime operations (live messaging and presence)
const unsubscribe = await blink.realtime.subscribe('chat-room', (message) => {
  console.log('New message:', message.data)
})

await blink.realtime.publish('chat-room', 'message', { text: 'Hello world!' })

// Get presence - returns array of PresenceUser objects directly
const users = await blink.realtime.presence('chat-room')
console.log('Online users:', users.length)
// users is PresenceUser[] format:
// [
//   { 
//     userId: 'user123', 
//     metadata: { displayName: 'Alice', status: 'online' },
//     joinedAt: 1640995200000,
//     lastSeen: 1640995230000
//   }
// ]

// Analytics operations (automatic pageview tracking + custom events)
// Pageviews are tracked automatically on initialization and route changes
blink.analytics.log('button_clicked', {
  button_id: 'signup',
  page: '/pricing'
})

// Check if analytics is enabled
if (blink.analytics.isEnabled()) {
  console.log('Analytics is active')
}

// Turn SDK analytics off from the start: the SDK sends and stores nothing
// (sites hosted on Blink also get a separate page-view tag, not controlled here)
// const blink = createClient({ projectId, analytics: false })

// Opt a visitor out (remembered on this device) or back in
blink.analytics.disable()
blink.analytics.enable()

// Storage operations (instant - returns public URL directly)
const { publicUrl } = await blink.storage.upload(
  file,
  `avatars/${user.id}-${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
  { upsert: true }
)

闪烁是 AI App 构建器 以秒数创建全功能应用程序。 简而言之,你想要构建的东西,而Blink的AI代理将:

  • 🏗️ 生成完整的应用程序 使用反应 + 类型脚本 + Vite
  • 🔧 自动安装此 SDK 配置为零
  • 🎨 创建美丽的 UIs 有尾风 CSS
  • 🚀 立即部署 具有认证、数据库、AI和存储内置

SDK 特性

这个 SDK 赋予每个 Blink 生成的应用程序 :

  • 认证: 具有管理(定向)和无头(海关UI)模式的灵活认证系统,电子邮件/密码,社交供应商(Google,GitHub,苹果,微软),魔法链接,RBAC,以及自定义电子邮件品牌
  • QQ 数据库: PostgREST 兼容 CRUD 操作,高级过滤
  • 大赦国际:多模式图像生成和编辑(10个模式),文本生成,同时进行网络搜索,对象生成,语音合成,并抄录
  • 数据: 从文档中取出文本内容, 以秘密替代安全 API 代理, 网络刮取, 截图和网络搜索
  • 📁 存储: 文件上传、下载和管理
  • * 通知:带有附件的电子邮件发送、定制品牌和交货跟踪
  • 实时: 基于 WebSocket 的 pub/ sub 消息、 存在跟踪和实时更新
  • 分析: 自动页面视图跟踪、自定义事件记录、会话管理以及隐私第一设计
  • 通用: 客户端和服务器端的工作
  • 框架不可知论:反应,Vue,斯维尔特,香草JS,Node.js,德诺, 原生反应
  • * 反应原生:具有AsyncStorage集成和平台意识功能的一等移动支持
  • 实时: 内置认证状态管理并刷新标志
  • 零波列尔普拉特: 一切都在盒子里运作

QQ 手动安装和设置

QQ 小费:如果你使用Blink的AI代理,这都是自动为你完成的!

客户端( 反应, Vue等)

import { createClient } from '@blinkdotnew/sdk'

const blink = createClient({
  projectId: 'your-blink-project-id', // From blink.new dashboard
  authRequired: false, // Let users browse first - require auth only for protected areas
  auth: {
    mode: 'managed' // Use new explicit configuration
  }
})

版本要求: 灵活的认证系统( 管理 vs 无头模式) 需要 SDK 版本 0.1.8.0或以上(含). 若使用0.17.x或以下版本,则只能访问遗留认证系统. 请升级以访问新的认证功能 .

服务器侧 (Node.js, Deno, Edge 函数)

import { createClient } from '@blinkdotnew/sdk'

const blink = createClient({
  projectId: 'your-blink-project-id', // From blink.new dashboard
  auth: { mode: 'managed' } // Manual token management
})

// Token injection is only needed when calling blink.auth.* methods on the server

QQ 本地反应( iOS & Android)

SDK已经 一等反应原生支持 拥有自动适应移动环境的平台意识功能.

步骤1:安装附属物

npm install @blinkdotnew/sdk @react-native-async-storage/async-storage expo-web-browser

需要的软件包 :

  • @blinkdotnew/sdk - 闪闪发光的SDK
  • @react-native-async-storage/async-storage - 为象征的坚持
  • expo-web-browser - 用于OAuth认证(Google, GitHub, Apple等)

步骤2: 创建客户端( E)lib/blink.ts(中文(简体) )

你一定要通过 webBrowser: WebBrowser 启用 OAuth 移动!

// lib/blink.ts
import { createClient, AsyncStorageAdapter } from '@blinkdotnew/sdk'
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as WebBrowser from 'expo-web-browser'  // ← Import this
import * as Linking from 'expo-linking'

export const blink = createClient({
  projectId: 'your-project-id',
  authRequired: false,
  auth: {
    mode: 'headless',
    webBrowser: WebBrowser,  // ← Pass it here! Required for OAuth
    linking: Linking         // ← Binds the sign-in to your app (needs a reverse-domain app.json scheme)
  },
  storage: new AsyncStorageAdapter(AsyncStorage)
})

与 linking 和一个倒置域 scheme 内 app.json (例如:. com.example.myapp), OAuth 返回到 您的应用程序有一次性代码, 只有您的应用程序可以收集登录。 没有他们(或在博览会) sDK又回到了旧的流量.

当应用程序启动时, SDK 会恢复保存的会话 AsyncStorage 独自一人。 在此之前 结束, onAuthStateChanged 报告 isLoading: true,所以等待它 之前显示登录.

第3步: 使用认证

电子邮件/密码(立即工作):

// Sign up
const user = await blink.auth.signUp({ 
  email: '[email protected]', 
  password: 'SecurePass123' 
})

// Sign in
const user = await blink.auth.signInWithEmail('[email protected]', 'SecurePass123')

OAuth (谷歌, GitHub, 苹果, 微软):

// ✅ Same code works on web, iOS, AND Android!
const user = await blink.auth.signInWithGoogle()
const user = await blink.auth.signInWithGitHub()
const user = await blink.auth.signInWithApple()
const user = await blink.auth.signInWithMicrosoft()

第4步: 创建 Auth Hook(hooks/useAuth.ts(中文(简体) )

// hooks/useAuth.ts
import { useEffect, useState } from 'react'
import { blink } from '@/lib/blink'
import type { BlinkUser } from '@blinkdotnew/sdk'

export function useAuth() {
  const [user, setUser] = useState<BlinkUser | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    const unsubscribe = blink.auth.onAuthStateChanged((state) => {
      setUser(state.user)
      setIsLoading(state.isLoading)
    })
    return unsubscribe
  }, [])

  return {
    user,
    isLoading,
    isAuthenticated: !!user,
    signInWithGoogle: () => blink.auth.signInWithGoogle(),
    signInWithGitHub: () => blink.auth.signInWithGitHub(),
    signInWithApple: () => blink.auth.signInWithApple(),
    signOut: () => blink.auth.signOut(),
  }
}

第5步:组件使用

import { useAuth } from '@/hooks/useAuth'
import { View, Text, Button } from 'react-native'

function App() {
  const { user, isLoading, signInWithGoogle, signOut } = useAuth()

  if (isLoading) return <Text>Loading...</Text>

  if (!user) {
    return <Button onPress={signInWithGoogle} title="Sign in with Google" />
  }

  return (
    <View>
      <Text>Welcome, {user.email}!</Text>
      <Button onPress={signOut} title="Sign Out" />
    </View>
  )
}

常见错误

// ❌ WRONG: Missing webBrowser - OAuth won't work on mobile!
const blink = createClient({
  projectId: 'your-project-id',
  auth: { mode: 'headless' },  // Missing webBrowser!
  storage: new AsyncStorageAdapter(AsyncStorage)
})

// ✅ CORRECT: Include webBrowser for OAuth support
import * as WebBrowser from 'expo-web-browser'

const blink = createClient({
  projectId: 'your-project-id',
  auth: { 
    mode: 'headless',
    webBrowser: WebBrowser  // ← Required for OAuth!
  },
  storage: new AsyncStorageAdapter(AsyncStorage)
})

低级 OAuth (用于自定义控制)

如果您需要自定义的投票超时或人工浏览器处理 :

// Get auth URL and authenticate function separately
const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google')

// Open browser manually
await WebBrowser.openAuthSessionAsync(authUrl)

// Poll with custom options
const user = await authenticate({
  maxAttempts: 120,  // 60 seconds (default: 60 = 30 seconds)
  intervalMs: 500    // Check every 500ms
})

平台特征

  • ✅ 同步组合 - 坚守安全
  • ✅ 通用操作 - 同一代码在网络+移动上有效
  • ✅ 前网络浏览器 - 本地浏览器UI
  • ✅ 无深层链接 - 按会期投票
  • ✅ 博览会 - 不需要自定义dev客户端
  • ✅ 自动签名刷新 - 无缝会议

API 参考

认证

版本要求: 灵活的认证系统需要SDK版本 0.1.8.0或以上(含). 0.17.x和以下版本只支持遗留的认证系统.

闪烁提供 两种认证模式数字 :

QQ 管理模式( 直接模式)

完美为: 快速设置, 最小代码 最佳服务: 网站,简易应用程序,MVP开发

const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'managed' }
})

// ONE METHOD: Redirect to hosted auth page
blink.auth.login() // → Redirects to blink.new/auth
blink.auth.logout() // Clear tokens and redirect

// User state (automatic after redirect)
const user = await blink.auth.me()

QQ 无头模式( Custom UI)

完美为: 自定义品牌、高级UX、移动应用程序 最佳服务: 生产应用、品牌经验

const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'headless' }
})

// MULTIPLE METHODS: Build your own UI
const user = await blink.auth.signUp({ email, password })
const user = await blink.auth.signInWithEmail(email, password)
const user = await blink.auth.signInWithGoogle()
const user = await blink.auth.signInWithGitHub()
const user = await blink.auth.signInWithApple()
const user = await blink.auth.signInWithMicrosoft()

// ✅ Store custom signup fields inside metadata
await blink.auth.signUp({
  email: '[email protected]',
  password: 'SuperSecret123',
  displayName: 'Alex Founder',
  role: 'operations',
  metadata: {
    company: 'Acme Freight',
    marketingConsent: true
  }
})
// `displayName`, `avatar`, and `role` map to dedicated auth columns.
// Everything else goes into auth.users.metadata automatically.
// Keep custom fields in metadata or your own profile table—avoid adding NOT NULL
// columns directly to auth tables.

// Magic links (passwordless)
await blink.auth.sendMagicLink(email)

// Password management
await blink.auth.sendPasswordResetEmail(email)
await blink.auth.sendPasswordResetEmail(email, { 
  redirectUrl: 'https://myapp.com/reset-password' 
})
await blink.auth.changePassword(oldPass, newPass)

// Email verification
await blink.auth.sendEmailVerification()

QQ 快速模式比较

特性管理模式无头模式
设置1行码需要自定义用户界面
方法login() 仅限signInWith*() 方法
用户主机认证页面您的自定义表格
标记闪闪发光完全自定义
移动网络重定向土著融合

🚨 常见错误

// ❌ WRONG: Using managed method in headless mode
const blink = createClient({
  auth: { mode: 'headless' }
})
blink.auth.login() // Still redirects! Wrong method for headless

// ✅ CORRECT: Use headless methods
await blink.auth.signInWithEmail(email, password)
await blink.auth.signInWithGoogle()

提供方配置

步骤1: 在您的工程中启用提供者

  1. 转到 眨一眼,换一眼 打开您的项目
  2. 导航到 项目-工作空间-认证
  3. 切换打开/关闭提供者 :
    • 电子邮件 QQ(默认启用) - 包括电子邮件/密码、魔法链接和校验
    • 谷歌 (默认启用)
    • GitHub 图像 qq( 默认残疾)
    • 苹果公司 qq( 默认残疾)
    • 微软 qq( 默认残疾)
  4. 配置电子邮件设置 :
    • 需要电子邮件验证: 默认关闭(容易执行)
    • 允许用户注册: 在默认情况下

步骤2:发现现有供应商

// Get providers enabled for your project
const availableProviders = await blink.auth.getAvailableProviders()
// Returns: ['email', 'google'] (based on your project settings)

// Use in your UI to show only enabled providers
const showGoogleButton = availableProviders.includes('google')
const showGitHubButton = availableProviders.includes('github')

第3步: 客户端- 系统过滤( 无头模式)

const blink = createClient({
  projectId: 'your-project',
  auth: {
    mode: 'headless'
    // All providers controlled via project settings
  }
})

管理模式: 自动提供者显示

const blink = createClient({
  projectId: 'your-project',
  auth: { mode: 'managed' }
})

// The hosted auth page automatically shows only enabled providers
blink.auth.login() // Shows Email + Google by default

电子邮件验证流程

默认情况下, 不需要电子邮件验证 以方便执行。 仅在需要时启用 :

步骤1:配置验证(可选)

  1. 转到 眨一眼,换一眼 项目 工作空间 认证
  2. 切换 "需要电子邮件验证" ON( 默认损坏)

第2步:处理整流

// User signup - always send verification email for security
try {
  const user = await blink.auth.signUp({ email, password })
  await blink.auth.sendEmailVerification()
  setMessage('Account created! Check your email to verify.')
} catch (error) {
  setError(error.message)
}

// User signin - handle verification requirement
try {
  await blink.auth.signInWithEmail(email, password)
  // Success - user is signed in
} catch (error) {
  if (error.code === 'EMAIL_NOT_VERIFIED') {
    setError('Please verify your email first')
    await blink.auth.sendEmailVerification() // Resend verification
  } else {
    setError(error.message)
  }
}

// Manual verification resend
await blink.auth.sendEmailVerification()

发生什么了

  1. 注册: 创建用户账户, email_verified = false
  2. 校验电子邮件: 用户点击链接 email_verified = true
  3. 签名检查: 如果需要核查而未经核查 □ EMAIL_NOT_VERIFIED 错误
  4. 成绩: 用户一旦核实(或不需要核实)即可签名

灵活电子邮件系统

最大灵活性 - 控制电子邮件品牌,而Brink处理安全标志:

generatePasswordResetToken() 财务报告和审定财务报表 generateMagicLinkToken() 只有服务器. 他们 返回可赎回的证书,所以他们需要客户端与 secretKey 并且现在 拒绝(403)否则。 从浏览器代码使用中 sendPasswordResetEmail() 编号 : sendMagicLink() 换句话说.

// Option A: Custom email delivery (full branding control) — SERVER-ONLY
const serverBlink = createClient({
  projectId: process.env.BLINK_PROJECT_ID,
  secretKey: process.env.BLINK_SECRET_KEY,
})
const resetData = await serverBlink.auth.generatePasswordResetToken('[email protected]')
// Returns: { token, expiresAt, resetUrl }

// Send with your email service and branding
await yourEmailService.send({
  to: '[email protected]',
  subject: 'Reset your YourApp password',
  html: `
    <div style="font-family: Arial, sans-serif;">
      <img src="https://yourapp.com/logo.png" alt="YourApp" />
      <h1>Reset Your Password</h1>
      <a href="${resetData.resetUrl}" 
         style="background: #0070f3; color: white; padding: 16px 32px; 
                text-decoration: none; border-radius: 8px;">
        Reset Password
      </a>
    </div>
  `
})

// Option B: Blink default email (zero setup)
await blink.auth.sendPasswordResetEmail('[email protected]')

// Email verification is scoped to the signed-in user, so it stays client-safe
const verifyData = await blink.auth.generateEmailVerificationToken()

// Magic links mint a sign-in credential — server-only, like Option A above
const magicData = await serverBlink.auth.generateMagicLinkToken('[email protected]')

角色接入控制

// Configure roles and permissions
const blink = createClient({
  projectId: 'your-project',
  auth: {
    mode: 'headless',
    roles: {
      admin: { permissions: ['*'] },
      editor: { permissions: ['posts.create', 'posts.update'] },
      viewer: { permissions: ['posts.read'] }
    }
  }
})

// Check permissions
const canEdit = blink.auth.can('posts.update')
const isAdmin = blink.auth.hasRole('admin')
const isStaff = blink.auth.hasRole(['admin', 'editor'])

// Use in components
function EditButton() {
  if (!blink.auth.can('posts.update')) return null
  return <button onClick={editPost}>Edit Post</button>
}

核心方法

QQ Tokens 为 Blink API 自动管理. 使用 getValidToken() 只有您必须手动将一个符号传递给您的后端或第三方服务.

// User management
const user = await blink.auth.me()
await blink.auth.updateMe({ displayName: 'New Name' })

// Token management
blink.auth.setToken(jwt, persist?)
const isAuth = blink.auth.isAuthenticated()
const token = await blink.auth.getValidToken() // Get valid token (auto-refreshes)

// Password management
await blink.auth.sendPasswordResetEmail('[email protected]')
await blink.auth.sendPasswordResetEmail('[email protected]', { 
  redirectUrl: 'https://myapp.com/reset-password' // Custom reset page
})
await blink.auth.changePassword('oldPass', 'newPass')
await blink.auth.confirmPasswordReset(token, newPassword)

// Email verification
await blink.auth.verifyEmail(token)

// Provider discovery
const providers = await blink.auth.getAvailableProviders()

// Auth state listener (REQUIRED for React apps!)
const unsubscribe = blink.auth.onAuthStateChanged((state) => {
  console.log('Auth state:', state)
  // state.user - current user or null
  // state.isLoading - true while auth is initializing
  // state.isAuthenticated - true if user is logged in
  // state.tokens - current auth tokens
})

会话在网络blips中幸存. SDK仅在认证服务器表示会话无效(过期,被撤销,被封禁,或被删除的账户)时才会将用户签名出局. 如果一个令牌刷新无法到达服务器——离线,当一个电话应用程序从背景恢复时,一个请求被放弃,或者一个临时的服务器错误——存储会话和用户被保留. 在下一个请求中重试了已丢弃的连接; 在服务器出错后, SDK 在重试前等待了几秒钟 . 虽然认证服务器无法访问, getValidToken() 返回当前访问符号,即使它已经过期(而不是 null),所以您的请求作为一个网络错误失败或获得一个 401 您可以重试—— 它返回 null 只有在没有会议的时候.

登录方向行为

何时 login() 名称, SDK 自动确定认证后方向:

// 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')
}

固定在v1.x中: SDK 现在确保重定向 URL 总是绝对的, 防止在 window.location.href 返回相对路径.

数据库操作

QQ新:自动大小写转换!
SDK现在在JavaScript骆驼Case和SQL蛇 case之间自动转换:

  • 表格名称数字 : blink.db.emailDrafts → email_drafts 图表
  • 字段名称数字 : userId, (中文(简体) ), createdAt, (中文(简体) ), isCompleted → user_id, (中文(简体) ), created_at, (中文(简体) ), is_completed
  • 不需要手动转换!

QQ 重要: 总是在您的代码中使用骆驼Case

  • ✅ 对数字 : blink.db.emailDrafts.create({ userId: user.id, createdAt: new Date() })
  • ❌ 错误数字 : blink.db.email_drafts.create({ user_id: user.id, created_at: new Date() })
// Create (ID auto-generated if not provided)
const todo = await blink.db.todos.create({
  id: 'todo_12345', // Optional - auto-generated if not provided
  title: 'Learn Blink SDK',
  userId: user.id,        // camelCase in code
  createdAt: new Date(),  // camelCase in code
  isCompleted: false      // camelCase in code
})

// Read with filtering - returns camelCase fields
const todos = await blink.db.todos.list({
  where: {
    AND: [
      { userId: user.id },                    // camelCase in filters
      { OR: [{ status: 'open' }, { priority: 'high' }] }
    ]
  },
  orderBy: { createdAt: 'desc' },           // camelCase in orderBy
  limit: 20
})
// `todos` is a direct array: Todo[]

// Note: Boolean fields are returned as "0"/"1" strings from SQLite
// Check boolean values using Number(value) > 0
const completedTodos = todos.filter(todo => Number(todo.isCompleted) > 0)
const incompleteTodos = todos.filter(todo => Number(todo.isCompleted) === 0)

// Update
await blink.db.todos.update(todo.id, { isCompleted: true })

// Delete
await blink.db.todos.delete(todo.id)

// Bulk operations (IDs auto-generated if not provided)
await blink.db.todos.createMany([
  { title: 'Task 1', userId: user.id }, // ID will be auto-generated
  { id: 'custom_id', title: 'Task 2', userId: user.id } // Custom ID provided
])
await blink.db.todos.upsertMany([...])


AI 操作

// Text generation (simple prompt)
const { text } = await blink.ai.generateText({
  prompt: 'Write a poem about coding',
  maxTokens: 150
})

// Web search (OpenAI only) - get real-time information
const { text, sources } = await blink.ai.generateText({
  prompt: 'Who is the current US president?',
  search: true // Returns current info + source URLs
})

// Multi-step reasoning - for complex analysis
const { text } = await blink.ai.generateText({
  prompt: 'Research and analyze tech trends',
  search: true,
  maxSteps: 10, // Override default (25 when tools used)
  experimental_continueSteps: true // Override default (true when tools used)
})

// Text generation with image content
// ⚠️ IMPORTANT: Images must be HTTPS URLs with file extensions (.jpg, .jpeg, .png, .gif, .webp)
// For file uploads, use blink.storage.upload() first to get public HTTPS URLs
// 🚫 CRITICAL: When uploading files, NEVER hardcode extensions - use file.name or auto-detection
const { text } = await blink.ai.generateText({
  messages: [
    { 
      role: "user", 
      content: [
        { type: "text", text: "What do you see in this image?" },
        { type: "image", image: "https://storage.googleapis.com/.../.../photo.jpg" }
      ]
    }
  ]
})

// Mixed content with multiple images
const { text } = await blink.ai.generateText({
  messages: [
    { 
      role: "user", 
      content: [
        { type: "text", text: "Compare these two images:" },
        { type: "image", image: "https://storage.googleapis.com/.../.../image1.jpg" },
        { type: "image", image: "https://cdn.example.com/image2.png" }
      ]
    }
  ]
})

// Structured object generation
const { object } = await blink.ai.generateObject({
  prompt: 'Generate a user profile',
  schema: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      age: { type: 'number' }
    }
  }
})

// ⚠️ IMPORTANT: Schema Rule for generateObject()
// The top-level schema MUST use type: "object" - you cannot use type: "array" at the top level
// This ensures clear, robust, and extensible API calls with named parameters

// ✅ Correct: Array inside object
const { object: todoList } = await blink.ai.generateObject({
  prompt: 'Generate a list of 5 daily tasks',
  schema: {
    type: 'object',
    properties: {
      tasks: {
        type: 'array',
        items: { 
          type: 'object',
          properties: {
            title: { type: 'string' },
            priority: { type: 'string', enum: ['low', 'medium', 'high'] }
          }
        }
      }
    },
    required: ['tasks']
  }
})
// Result: { tasks: [{ title: "Exercise", priority: "high" }, ...] }

// ❌ Wrong: Top-level array (will fail)
// const { object } = await blink.ai.generateObject({
//   prompt: 'Generate tasks',
//   schema: {
//     type: 'array',  // ❌ This will throw an error
//     items: { type: 'string' }
//   }
// })
// Error: "schema must be a JSON Schema of 'type: \"object\"', got 'type: \"array\"'"

// Generate and modify images with AI - Multi-Model Support (10 models available)
// 🔥 Choose between fast generation or high-quality results
// 🎨 For style transfer: provide ALL images in the images array, don't reference URLs in prompts

// Basic image generation (uses default fast model: fal-ai/nano-banana)
const { data } = await blink.ai.generateImage({
  prompt: 'A serene landscape with mountains and a lake at sunset'
})
console.log('Image URL:', data[0].url)

// High-quality generation with Pro model
const { data: proImage } = await blink.ai.generateImage({
  prompt: 'A detailed infographic about AI with charts and diagrams',
  model: 'fal-ai/nano-banana-pro', // High quality model
  n: 1,
  size: '1792x1024' // Custom size
})

// Generate multiple variations
const { data } = await blink.ai.generateImage({
  prompt: 'A futuristic robot in different poses',
  model: 'fal-ai/nano-banana', // Fast model
  n: 3
})
data.forEach((img, i) => console.log(`Image ${i+1}:`, img.url))

**Available Models for Text-to-Image:**

| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `fal-ai/nano-banana` (default) | ⚡ Fast | Good | Prototypes, high-volume generation |
| `fal-ai/nano-banana-pro` | Standard | ⭐ Excellent | Marketing materials, high-fidelity visuals |
| `fal-ai/gemini-25-flash-image` | ⚡ Fast | Good | Alias for `nano-banana` |
| `fal-ai/gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Alias for `nano-banana-pro` |
| `gemini-2.5-flash-image-preview` | ⚡ Fast | Good | Legacy - Direct Gemini API |
| `gemini-3-pro-image-preview` | Standard | ⭐ Excellent | Legacy - Direct Gemini API |

// Image editing - transform existing images with prompts (uses default fast model)
const { data: headshots } = await blink.ai.modifyImage({
  images: ['https://storage.example.com/user-photo.jpg'], // Up to 16 images supported!
  prompt: 'Transform into professional business headshot with studio lighting'
})

// High-quality editing with Pro model
const { data: proEdited } = await blink.ai.modifyImage({
  images: ['https://storage.example.com/portrait.jpg'],
  prompt: 'Add a majestic ancient tree in the background with glowing leaves',
  model: 'fal-ai/nano-banana-pro/edit' // High quality editing
})

// Advanced image editing with multiple input images
const { data } = await blink.ai.modifyImage({
  images: [
    'https://storage.example.com/photo1.jpg',
    'https://storage.example.com/photo2.jpg',
    'https://storage.example.com/photo3.jpg'
  ],
  prompt: 'Combine these architectural styles into a futuristic building design',
  model: 'fal-ai/nano-banana/edit', // Fast editing
  n: 2
})

**Available Models for Image Editing:**

| Model | Speed | Quality | Best For |
|-------|-------|---------|----------|
| `fal-ai/nano-banana/edit` (default) | ⚡ Fast | Good | Quick adjustments, style transfers |
| `fal-ai/nano-banana-pro/edit` | Standard | ⭐ Excellent | Detailed retouching, complex edits |
| `fal-ai/gemini-25-flash-image/edit` | ⚡ Fast | Good | Alias for `nano-banana/edit` |
| `fal-ai/gemini-3-pro-image-preview/edit` | Standard | ⭐ Excellent | Alias for `nano-banana-pro/edit` |

// 🎨 Style Transfer & Feature Application
// ⚠️ IMPORTANT: When applying styles/features from one image to another,
// provide ALL images in the array - don't reference images in the prompt text

// ❌ WRONG - Don't reference images in prompt
const wrongWay = await blink.ai.modifyImage({
  images: [userPhotoUrl],
  prompt: `Apply this hairstyle from the reference image to the person's head. Reference hairstyle: ${hairstyleUrl}`
})

// ✅ CORRECT - Provide all images in the array
const { data } = await blink.ai.modifyImage({
  images: [userPhotoUrl, hairstyleUrl], // Both images provided
  prompt: 'Apply the hairstyle from the second image to the person in the first image. Blend naturally with face shape.'
})

// More style transfer examples
const { data } = await blink.ai.modifyImage({
  images: [portraitUrl, artworkUrl],
  prompt: 'Apply the artistic style and color palette from the second image to the portrait in the first image'
})

const { data } = await blink.ai.modifyImage({
  images: [roomPhotoUrl, designReferenceUrl],
  prompt: 'Redesign the room in the first image using the interior design style shown in the second image'
})

// Multiple reference images for complex transformations
const { data } = await blink.ai.modifyImage({
  images: [
    originalPhotoUrl,
    lightingReferenceUrl,
    colorPaletteReferenceUrl,
    compositionReferenceUrl
  ],
  prompt: 'Transform the first image using the lighting style from image 2, color palette from image 3, and composition from image 4'
})

// 📱 File Upload + Style Transfer
// ⚠️ Extract file extension properly - never hardcode .jpg/.png

// ❌ WRONG - Hardcoded extension
const userUpload = await blink.storage.upload(file, `photos/${Date.now()}.jpg`) // Breaks HEIC/PNG files
// ✅ CORRECT - Extract original extension
const userUpload = await blink.storage.upload(
  userPhoto.file, 
  `photos/${Date.now()}.${userPhoto.file.name.split('.').pop()}`
)
const hairstyleUpload = await blink.storage.upload(
  hairstylePhoto.file,
  `haircuts/${Date.now()}.${hairstylePhoto.file.name.split('.').pop()}`
)

const { data } = await blink.ai.modifyImage({
  images: [userUpload.publicUrl, hairstyleUpload.publicUrl],
  prompt: 'Apply hairstyle from second image to person in first image'
})


// Speech synthesis
const { url } = await blink.ai.generateSpeech({
  text: 'Hello, world!',
  voice: 'nova'
})

// Audio transcription - Multiple input formats supported
// 🔥 Most common: Browser audio recording → Base64 → Transcription
let mediaRecorder: MediaRecorder;
let audioChunks: Blob[] = [];

// Step 1: Start recording
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);

mediaRecorder.ondataavailable = (event) => {
  audioChunks.push(event.data);
};

mediaRecorder.start();

// Step 2: Stop recording and transcribe
mediaRecorder.onstop = async () => {
  const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
  
  // SAFE method for large files - use FileReader (recommended)
  const base64 = await new Promise<string>((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => {
      const dataUrl = reader.result as string;
      const base64Data = dataUrl.split(',')[1]; // Extract base64 part
      resolve(base64Data);
    };
    reader.onerror = reject;
    reader.readAsDataURL(audioBlob);
  });
  
  // Transcribe using base64 (preferred method)
  const { text } = await blink.ai.transcribeAudio({
    audio: base64,  // Raw base64 string
    language: 'en'
  });
  
  console.log('Transcription:', text);
  audioChunks = []; // Reset for next recording
};

// Alternative: Data URL format (also supported)
const reader = new FileReader();
reader.onload = async () => {
  const dataUrl = reader.result as string;
  const { text } = await blink.ai.transcribeAudio({
    audio: dataUrl,  // Data URL format
    language: 'en'
  });
};
reader.readAsDataURL(audioBlob);

// File upload transcription
const fileInput = document.getElementById('audioFile') as HTMLInputElement;
const file = fileInput.files[0];

// Option 1: Convert to base64 using FileReader (recommended for large files)
const base64Audio = await new Promise<string>((resolve, reject) => {
  const reader = new FileReader();
  reader.onload = () => {
    const dataUrl = reader.result as string;
    const base64Data = dataUrl.split(',')[1]; // Extract base64 part
    resolve(base64Data);
  };
  reader.onerror = reject;
  reader.readAsDataURL(file);
});

const { text } = await blink.ai.transcribeAudio({
  audio: base64Audio,
  language: 'en'
});

// Option 2: Use ArrayBuffer directly (works for any file size)
const arrayBuffer = await file.arrayBuffer();
const { text } = await blink.ai.transcribeAudio({
  audio: arrayBuffer,
  language: 'en'
});

// Option 3: Use Uint8Array directly (works for any file size)
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const { text } = await blink.ai.transcribeAudio({
  audio: uint8Array,
  language: 'en'
});

// Public URL transcription (for hosted audio files)
const { text } = await blink.ai.transcribeAudio({
  audio: 'https://example.com/audio/meeting.mp3',
  language: 'en'
});

// Advanced options
const { text } = await blink.ai.transcribeAudio({
  audio: base64Audio,
  language: 'en',
  model: 'whisper-1',
  response_format: 'verbose_json'  // Get timestamps and confidence scores
});

// Supported audio formats: MP3, WAV, M4A, FLAC, OGG, WebM
// Supported input types:
// - string: Base64 data, Data URL (data:audio/...;base64,...), or public URL (https://...)
// - ArrayBuffer: Raw audio buffer (works for any file size)
// - Uint8Array: Audio data as byte array (works for any file size)
// - number[]: Audio data as number array

// ⚠️ IMPORTANT: For large audio files, use FileReader or ArrayBuffer/Uint8Array
// Avoid btoa(String.fromCharCode(...array)) as it crashes with large files

// Streaming support
await blink.ai.streamText(
  { prompt: 'Write a story...' },
  (chunk) => console.log(chunk)
)

// Streaming with web search
await blink.ai.streamText(
  { prompt: 'Latest AI news', search: true },
  (chunk) => console.log(chunk)
)

// React streaming example - parse chunks for immediate UI display
const [streamingText, setStreamingText] = useState('')

await blink.ai.streamText(
  { prompt: 'Write a story about AI...' },
  (chunk) => {
    setStreamingText(prev => prev + chunk) // chunk is a string
  }
)

数据操作

// Simple text extraction (default - returns single string)
const text = await blink.data.extractFromUrl('https://example.com/document.pdf');
console.log(typeof text); // 'string'

// Extract with chunking enabled
const chunks = await blink.data.extractFromUrl('https://example.com/document.pdf', {
  chunking: true,
  chunkSize: 2000
});
console.log(Array.isArray(chunks)); // true

// Extract from different file types
const csvText = await blink.data.extractFromUrl('https://example.com/data.csv');
const htmlText = await blink.data.extractFromUrl('https://example.com/page.html');
const jsonText = await blink.data.extractFromUrl('https://example.com/config.json');

// Extract from uploaded file blob (simple)
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const file = fileInput.files[0];
const extractedText = await blink.data.extractFromBlob(file);

// Extract from uploaded file blob (with chunking)
const chunks = await blink.data.extractFromBlob(file, {
  chunking: true,
  chunkSize: 3000
});

// Website scraping (NEW!) - Crystal clear destructuring
const { markdown, metadata, links, extract } = await blink.data.scrape('https://example.com');
console.log(markdown); // Clean markdown content
console.log(metadata.title); // Page title
console.log(links.length); // Number of links found

// Even cleaner - destructure only what you need
const { metadata, extract } = await blink.data.scrape('https://blog.example.com/article');
console.log(metadata.title); // Always available
console.log(extract.headings); // Always an array

// Website screenshots (NEW!)
const screenshotUrl = await blink.data.screenshot('https://example.com');
console.log(screenshotUrl); // Direct URL to screenshot image

// Full-page screenshot with custom dimensions
const fullPageUrl = await blink.data.screenshot('https://example.com', {
  fullPage: true,
  width: 1920,
  height: 1080
});

// 🔥 Web Search (NEW!) - Google search results with clean structure
// Perfect for getting real-time information and current data

// Basic web search - just provide a query
const searchResults = await blink.data.search('chatgpt');
console.log(searchResults.organic_results); // Main search results
console.log(searchResults.related_searches); // Related search suggestions
console.log(searchResults.people_also_ask); // People also ask questions

// Search with location for local results
const localResults = await blink.data.search('best restaurants', {
  location: 'San Francisco,CA,United States'
});
console.log(localResults.local_results); // Local business results
console.log(localResults.organic_results); // Regular web results

// News search - get latest news articles
const newsResults = await blink.data.search('artificial intelligence', {
  type: 'news'
});
console.log(newsResults.news_results); // News articles with dates and sources

// Image search - find images
const imageResults = await blink.data.search('elon musk', {
  type: 'images',
  limit: 20
});
console.log(imageResults.image_results); // Image results with thumbnails

// Search in different languages
const spanishResults = await blink.data.search('noticias tecnología', {
  language: 'es',
  type: 'news'
});

// Shopping search - find products
const shoppingResults = await blink.data.search('macbook pro', {
  type: 'shopping'
});
console.log(shoppingResults.shopping_results); // Product results with prices

// All search types return consistent, structured data:
// - organic_results: Main search results (always included)
// - related_searches: Related search suggestions
// - people_also_ask: FAQ-style questions and answers
// - local_results: Local businesses (when location provided)
// - news_results: News articles (when type='news')
// - image_results: Images (when type='images')
// - shopping_results: Products (when type='shopping')
// - ads: Sponsored results (when present)

// 🔥 Secure API Proxy (NEW!) - Make API calls with secret substitution

// Basic API call with secret substitution
const response = await blink.data.fetch({
  url: 'https://api.sendgrid.com/v3/mail/send',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer {{sendgrid_api_key}}', // Secret replaced server-side
    'Content-Type': 'application/json'
  },
  body: {
    from: { email: '[email protected]' },
    personalizations: [{ to: [{ email: '[email protected]' }] }],
    subject: 'Hello from Blink',
    content: [{ type: 'text/plain', value: 'Sent securely through Blink!' }]
  }
});

console.log('Email sent:', response.status === 200);
console.log('Response:', response.body);
console.log('Took:', response.durationMs, 'ms');

// GET request with secret in URL and query params
const weatherData = await blink.data.fetch({
  url: 'https://api.openweathermap.org/data/2.5/weather',
  method: 'GET',
  query: {
    q: 'London',
    appid: '{{openweather_api_key}}', // Secret replaced in query params
    units: 'metric'
  }
});

console.log('Weather:', weatherData.body.main.temp, '°C');

// Async/background requests (fire-and-forget)
const asyncResponse = await blink.data.fetchAsync({
  url: 'https://api.stripe.com/v1/customers',
  method: 'POST',
  headers: {
    'Authorization': 'Bearer {{stripe_secret_key}}',
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: '[email protected]&name=John Doe'
});

console.log(asyncResponse.status); // 'triggered'
console.log(asyncResponse.message); // 'Request triggered in background'

// Multiple secrets in different places
const complexRequest = await blink.data.fetch({
  url: 'https://api.github.com/repos/{{github_username}}/{{repo_name}}/issues',
  method: 'POST',
  headers: {
    'Authorization': 'token {{github_token}}',
    'Accept': 'application/vnd.github.v3+json',
    'User-Agent': '{{app_name}}'
  },
  body: {
    title: 'Bug Report',
    body: 'Found via {{app_name}} monitoring'
  }
});

// Secret substitution works everywhere:
// - URL path: /api/{{version}}/users
// - Query params: ?key={{api_key}}&user={{user_id}}
// - Headers: Authorization: Bearer {{token}}
// - Body: { "apiKey": "{{secret}}", "data": "{{value}}" }

// Error handling for data extraction
try {
  const result = await blink.data.extractFromUrl('https://example.com/huge-file.pdf');
} catch (error) {
  if (error instanceof BlinkDataError) {
    console.error('Data processing error:', error.message);
  }
}

存储操作

// Upload files (returns public URL directly)
const { publicUrl } = await blink.storage.upload(
  file,
  `uploads/${Date.now()}.${file.name.split('.').pop()}`, // ✅ Extract original extension
  {
    upsert: true,
    onProgress: (percent) => console.log(`${percent}%`)
  }
)

// ❌ WRONG - Hardcoded extensions break HEIC/PNG/WebP files
const wrong = await blink.storage.upload(file, `uploads/${Date.now()}.jpg`) // Corrupts non-JPG files
// ✅ CORRECT - Extract file extension
const correct = await blink.storage.upload(file, `uploads/${Date.now()}.${file.name.split('.').pop()}`)

// Remove files
await blink.storage.remove('file1.jpg', 'file2.jpg')

通知业务

// 🔥 Email Notifications (NEW!) - Send emails with attachments, custom branding, and delivery tracking

// Send a simple email - returns success status and message ID
const result = await blink.notifications.email({
  to: '[email protected]',
  subject: 'Your order has shipped!',
  html: '<h1>Order Confirmation</h1><p>Your order #12345 is on its way.</p>'
})

console.log(result.success)    // true/false - whether email was sent
console.log(result.messageId)  // "msg_abc123..." - unique message identifier

// Send with plain text fallback (recommended for better deliverability)
const { success, messageId } = await blink.notifications.email({
  to: '[email protected]',
  subject: 'Welcome to our platform!',
  html: '<h1>Welcome!</h1><p>Thanks for joining us.</p>',
  text: 'Welcome!\n\nThanks for joining us.' // Plain text version
})

// Send an email with attachments and custom branding
const result = await blink.notifications.email({
  to: ['[email protected]', '[email protected]'],
  from: '[email protected]', // Must be valid email address
  replyTo: '[email protected]',
  subject: 'New Invoice #12345',
  html: `
    <div style="font-family: Arial, sans-serif;">
      <h2>Invoice Ready</h2>
      <p>Please find the invoice attached.</p>
    </div>
  `,
  text: 'Invoice Ready\n\nPlease find the invoice attached.',
  cc: '[email protected]',
  bcc: '[email protected]',
  attachments: [
    { 
      url: 'https://mycompany.com/invoices/12345.pdf',
      filename: 'Invoice-12345.pdf', // Custom filename
      type: 'application/pdf'        // MIME type (optional)
    },
    {
      url: 'https://mycompany.com/terms.pdf',
      filename: 'Terms-of-Service.pdf'
    }
  ]
})

console.log(`Email ${result.success ? 'sent' : 'failed'}`)
console.log(`Message ID: ${result.messageId}`)

// Send to multiple recipients with different recipient types
const { success, messageId } = await blink.notifications.email({
  to: ['[email protected]', '[email protected]'],
  cc: ['[email protected]'],
  bcc: ['[email protected]', '[email protected]'],
  from: '[email protected]',
  subject: 'Monthly Newsletter',
  html: '<h2>This Month\'s Updates</h2><p>Here are the highlights...</p>'
})

// Dynamic email content with user data
const user = await blink.auth.me()
const welcomeEmail = await blink.notifications.email({
  to: user.email,
  from: '[email protected]',
  subject: `Welcome ${user.displayName}!`,
  html: `
    <h1>Hi ${user.displayName}!</h1>
    <p>Welcome to our platform. Your account is now active.</p>
    <p>Account ID: ${user.id}</p>
    <a href="https://myapp.com/dashboard">Get Started</a>
  `,
  text: `Hi ${user.displayName}!\n\nWelcome to our platform. Your account is now active.\nAccount ID: ${user.id}\n\nGet Started: https://myapp.com/dashboard`
})

// Comprehensive error handling with detailed error information
try {
  const result = await blink.notifications.email({
    to: '[email protected]',
    subject: 'Important Update',
    html: '<p>This is an important update about your account.</p>'
  })
  
  if (result.success) {
    console.log('✅ Email sent successfully!')
    console.log('📧 Message ID:', result.messageId)
  } else {
    console.error('❌ Email failed to send')
    // Handle failed send (retry logic, fallback notification, etc.)
  }
  
} catch (error) {
  if (error instanceof BlinkNotificationsError) {
    console.error('❌ Email error:', error.message)
    
    // Common error scenarios:
    // - "The 'to', 'subject', and either 'html' or 'text' fields are required."
    // - "Invalid email address format"
    // - "Attachment URL must be accessible"
    // - "Failed to send email: Rate limit exceeded"
    
    // Handle specific error types
    if (error.message.includes('Rate limit')) {
      // Implement retry with backoff
      console.log('⏳ Rate limited, will retry later')
    } else if (error.message.includes('Invalid email')) {
      // Log invalid email for cleanup
      console.log('📧 Invalid email address, removing from list')
    }
  } else {
    console.error('❌ Unexpected error:', error)
  }
}

// Email validation and best practices
const validateAndSendEmail = async (recipient: string, subject: string, content: string) => {
  // Basic validation
  if (!recipient.includes('@') || !subject.trim() || !content.trim()) {
    throw new Error('Invalid email parameters')
  }
  
  try {
    const result = await blink.notifications.email({
      to: recipient,
      from: '[email protected]',
      subject: subject,
      html: `
        <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
          <div style="background: #f8f9fa; padding: 20px; text-align: center;">
            <h1 style="color: #333; margin: 0;">My Company</h1>
          </div>
          <div style="padding: 20px;">
            ${content}
          </div>
          <div style="background: #f8f9fa; padding: 15px; text-align: center; font-size: 12px; color: #666;">
            <p>© 2024 My Company. All rights reserved.</p>
            <p><a href="https://mycompany.com/unsubscribe">Unsubscribe</a></p>
          </div>
        </div>
      `,
      text: content.replace(/<[^>]*>/g, '') // Strip HTML for text version
    })
    
    return result
  } catch (error) {
    console.error(`Failed to send email to ${recipient}:`, error)
    throw error
  }
}

// Usage with validation
try {
  const result = await validateAndSendEmail(
    '[email protected]',
    'Account Verification Required',
    '<p>Please verify your account by clicking the link below.</p><a href="https://myapp.com/verify">Verify Account</a>'
  )
  console.log('Email sent with ID:', result.messageId)
} catch (error) {
  console.error('Email validation or sending failed:', error.message)
}

// Bulk email sending with error handling
const sendBulkEmails = async (recipients: string[], subject: string, htmlContent: string) => {
  const results = []
  
  for (const recipient of recipients) {
    try {
      const result = await blink.notifications.email({
        to: recipient,
        from: '[email protected]',
        subject,
        html: htmlContent,
        text: htmlContent.replace(/<[^>]*>/g, '')
      })
      
      results.push({ 
        recipient, 
        success: result.success, 
        messageId: result.messageId 
      })
      
      // Rate limiting: wait between sends
      await new Promise(resolve => setTimeout(resolve, 100))
      
    } catch (error) {
      results.push({ 
        recipient, 
        success: false, 
        error: error.message 
      })
    }
  }
  
  return results
}

// Response format details:
// ✅ Success response: { success: true, messageId: "msg_abc123...", from: "[email protected]", to: ["[email protected]"], subject: "Email Subject", timestamp: "2024-01-20T10:30:00.000Z" }
// ❌ The method throws BlinkNotificationsError on failure
// 🔍 Error types: validation errors, rate limits, network issues, invalid attachments

// API Response Format:
// The notifications API returns data directly (not wrapped in {data: ..., error: ...})
// This is consistent with other Blink APIs like database and storage
// All Blink APIs follow this pattern for clean, predictable responses

// Best practices:
// 1. Always include both HTML and text versions for better deliverability
// 2. Use valid email addresses for 'from' field (not display names)
// 3. Keep HTML simple with inline CSS for email client compatibility
// 4. Handle rate limits with retry logic
// 5. Validate email addresses before sending
// 6. Use message IDs for tracking and debugging
// 7. Include unsubscribe links for compliance

分析操作

// 🔥 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, session_id (no user IDs or emails)
// - 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
// createClient({ projectId, analytics: false }) → SDK analytics off from the start: nothing sent or stored
// (sites hosted on Blink also get a separate page-view tag, not controlled here)
blink.analytics.disable() // stops sending, clears queued events and stored IDs, remembers the opt-out
blink.analytics.enable()  // opts back in
const isEnabled = blink.analytics.isEnabled()
// Off automatically when the browser sends Do Not Track or Global Privacy Control

// 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)

实时操作

零波纹连接管理!
所有连接状态,排队,再连接都是自动处理的. 别再犯"状态"错误了!

QQ 反应用户: 见 反应 + 实时连接 下面的章节,用于适当的ASync清理模式,以避免“订阅已取消”错误.

// 🔥 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)
  }
}

高级用途

处理错误

import { 
  BlinkAuthError, 
  BlinkAuthErrorCode,
  BlinkAIError, 
  BlinkStorageError, 
  BlinkDataError, 
  BlinkRealtimeError, 
  BlinkNotificationsError 
} from '@blinkdotnew/sdk'

// Authentication error handling
try {
  const user = await blink.auth.signInWithEmail(email, password)
} catch (error) {
  if (error instanceof BlinkAuthError) {
    switch (error.code) {
      case BlinkAuthErrorCode.EMAIL_NOT_VERIFIED:
        console.log('Email verification required')
        await blink.auth.sendEmailVerification()
        break
      case BlinkAuthErrorCode.INVALID_CREDENTIALS:
        console.error('Invalid email or password')
        break
      case BlinkAuthErrorCode.RATE_LIMITED:
        console.error('Too many attempts, try again later')
        break
      default:
        console.error('Auth error:', error.message)
    }
  }
}

// Other error types
try {
  const { text } = await blink.ai.generateText({ prompt: 'Hello' })
} catch (error) {
  if (error instanceof BlinkAIError) {
    console.error('AI error:', error.message)
  }
}

自定义配置

const blink = createClient({
  projectId: 'your-project',
  baseUrl: 'https://custom-api.example.com',
  auth: {
    mode: 'headless', // 'managed' | 'headless'
    authUrl: 'https://your-auth-service.com', // Custom auth domain (for all auth endpoints)
    coreUrl: 'https://custom-core.example.com', // Custom API domain (for db, ai, storage)
    // Providers controlled via project settings
    redirectUrl: 'https://myapp.com/dashboard',
    roles: {
      admin: { permissions: ['*'] },
      editor: { permissions: ['posts.create', 'posts.update'], inherit: ['viewer'] },
      viewer: { permissions: ['posts.read'] }
    }
  },
  httpClient: {
    timeout: 30000,
    retries: 3
  }
})

类型脚本支持

SDK是用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[]

API 代理服务器的秘密管理

这个 blink.data.fetch() 方法允许您用自动秘密替代方式安全地拨打API。 下面是如何设置:

步骤一: 将您的秘密存储在您的 Blink 项目中 访问您的项目仪表板 眨一眼,换一眼 并添加您的 API 密钥到“ 秘密” 部分 :

  • sendgrid_api_key → SG.abc123...
  • openweather_api_key → d4f5g6h7...
  • stripe_secret_key → sk_live_abc123...

第2步:在您的 API 呼叫中使用秘密

// 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
  }
})

第3步:秘密替代在任何地方都起作用

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'
  }
})

全体 {{secret_name}} 占位符被您项目秘密存储的加密值所取代。 秘密从不离开服务器 也永远看不到前端代码.

框架实例

反应 + 实时连接

QQ 关键: 避免多个 WebSocket 连接

最常见的错误是在使用效果中使用失去清理功能的同源函数:

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 cha

版本与发布记录

@blinkdotnew/sdk 共发布 120 个版本,首次发布于 2025/06/28;过去 12 个月发布了 47 个版本。

版本发布日期距今
2.11.4latest2026/11/043 天前
2.11.32026/10/309 天前
2.11.22026/10/309 天前
2.11.12026/10/2811 天前
2.11.02026/10/1425 天前
2.10.12026/09/301 个月前

关于 @blinkdotnew/sdk 的常见问题

@blinkdotnew/sdk 是什么?

用于客户端应用程序的 Blink TypeScript SDK - 0-boilerplate CRUD + auth + AI + 分析 + 现代 SaaS/AI 应用程序的通知

如何安装 @blinkdotnew/sdk?

在项目目录运行 npm install @blinkdotnew/sdk,或使用 pnpm add / yarn add / bun add。

@blinkdotnew/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@blinkdotnew/sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@blinkdotnew/sdk 可以商用吗?

@blinkdotnew/sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@blinkdotnew/sdk 的下载量有多少?

根据 npm 官方下载统计,@blinkdotnew/sdk 在 2026/10/09 – 2026/11/07 这 30 天内被下载 13,382 次,日均约 446 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@blinkdotnew/sdk 还在维护吗?

最新版本 2.11.4 发布于 2026/11/04(3 天前);至今共发布 120 个版本,其中过去 12 个月发布了 47 个;首次发布于 2025/06/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。