Migrate from Vercel
Migrate projects deployed on Vercel (static sites, Next.js, Serverless Functions, etc.) to Tencent CloudBase
Migration Overview
Vercel and CloudBase are highly aligned in core capabilities — both offer frontend + serverless backend + data storage as a one-stop deployment experience. Migration focuses on adapting six modules: static hosting, backend compute (Cloud Functions & CloudRun), databases, file storage, authentication, and environment variables.
| Dimension | Vercel | CloudBase |
|---|---|---|
| Static Hosting | Auto build & deploy from Git | Static website hosting + CDN acceleration |
| Serverless Functions | Vercel Functions (Node/Go/Py/Ruby) | Cloud Functions (Node.js / PHP / Python / Golang / Java) + CloudBase Run (any language/framework) |
| Edge Functions | Edge Functions / Middleware | CloudRun Function Framework (supports SSE, WebSocket) |
| Database | Vercel Marketplace (Neon Postgres / Supabase, etc.) | Document Database (NoSQL) + MySQL Database + PostgreSQL Database |
| Object Storage | Vercel Blob Storage | Cloud Storage (built-in CDN + image processing) |
| Authentication | Third-party integrations (Clerk / Auth0 / NextAuth) | Built-in authentication (WeChat / SMS / Email / Anonymous / Custom) |
| Custom Domain | One-click binding from Dashboard | Console binding + self-upload SSL certificate |
| Environment Variables | Dashboard / CLI management | Console / CLI management |
Quick Reference
| My current Vercel usage | CloudBase equivalent |
|---|---|
| Deploy static sites (Vite/React/Vue) | tcb app deploy -e <envId> |
| Deploy Next.js SSR | CloudBase Run + Dockerfile |
| Deploy Next.js static export | Same as static sites |
api/*.ts Serverless Functions | Cloud Functions or CloudBase Run Express app |
middleware.ts edge logic | CloudBase Run middleware / API Gateway |
Environment variables process.env.XXX | Configure in console, no code changes needed |
| Custom domains | Console "Domain Management" binding |
| Vercel Postgres (Marketplace · Neon) | CloudBase PostgreSQL Database / MySQL Database |
| Vercel Blob Storage | CloudBase Cloud Storage |
Module 1: Static Hosting
CloudBase Static Website Hosting: built-in CDN, HTTPS, SPA Fallback, HTTP response headers, routing rules. Console: Static Website Hosting.
1. Pure Static Sites (Vite / Vue / React / Svelte / Astro / Hugo)
Applicable when: build output is pure HTML/CSS/JS, no dependency on Vercel Functions
# Install CLI
npm install -g @cloudbase/cli
# Login
tcb login
# One-click build + deploy (auto-detects framework, runs install → build → upload → route binding)
tcb app deploy -e <envId>
# Or specify framework type
tcb app deploy --framework vite -e <envId>
# Deploy to sub-path /app
tcb app deploy --deploy-path /app -e <envId>
A single command replaces Vercel's Git auto-deployment.
tcb hosting deployis only for uploading pre-built static files and is not the standard app deployment path.
2. Static Framework Deployment (Next.js / Nuxt.js / SvelteKit / Astro)
If using the framework's static export mode (build output is pure HTML/CSS/JS):
| Framework | Static Export Configuration | Deploy Command |
|---|---|---|
| Next.js | Set output: 'export' in next.config.js | tcb app deploy --framework next --output-dir out -e <envId> |
| Nuxt.js | Set ssr: false in nuxt.config.ts | tcb app deploy -e <envId> |
| SvelteKit | Configure @sveltejs/adapter-static in svelte.config.js | tcb app deploy -e <envId> |
| Astro | Default is static (output: 'static') | tcb app deploy -e <envId> |
tcb app deployauto-detects framework type and runsinstall → build → upload → route binding. Next.js requires explicit output directory specification; other frameworks are auto-detected.
Module 2: Backend Compute (Cloud Functions & CloudBase Run)
Vercel Functions alternatives come in two forms: lightweight standalone APIs use Cloud Functions, multi-route / SSR framework scenarios use CloudBase Run. Choose based on complexity.
Scenario Overview
| Vercel Usage | CloudBase Solution | Best For |
|---|---|---|
A few standalone files under api/ | Cloud Functions | Few APIs, independent deployment per function, minimal changes |
5+ files under api/ or need route management | CloudBase Run (Express/Koa) | Avoid function fragmentation, unified routing and middleware |
| Next.js / Nuxt / SvelteKit / Astro | CloudBase Run (Docker) | Framework-native SSR and server routes need container environment |
1. Scenario 1: Pure API Routes → Cloud Functions
CloudBase Cloud Functions: Node.js / PHP / Python / Golang / Java runtimes, HTTP/timer/database triggers, max 900s timeout. Console: Cloud Functions.
Capability Comparison
| Dimension | Vercel Functions | CloudBase Cloud Functions |
|---|---|---|
| Runtime | Node.js / Go / Python / Ruby | Node.js / PHP / Python / Golang / Java |
| Trigger | HTTP path auto-routing | HTTP trigger / Timer trigger / Database trigger |
| Path Mapping | api/*.ts → /api/* | One path per function |
| Timeout | 10s (Hobby) / 60s (Pro) | Up to 900s |
| Memory | 128MB - 3008MB | 256MB - 3072MB |
| Cold Start | Fast (edge deployment) | Configurable keep-alive (minNum >= 1) |
Directory Structure Migration
Vercel project structure:
api/
hello.ts → /api/hello
users/[id].ts → /api/users/:id
webhook.ts → /api/webhook
CloudBase Cloud Functions structure:
cloudfunctions/
hello/
index.js
package.json
users/
index.js
package.json
Tip: If you have many files under
api/but not yet ready to consolidate into CloudBase Run, use thebatch-migrate-functions.jsscript in the "Migration Script Tools" section at the end to generate the above skeleton with one command.
Code Example
Vercel (api/hello.ts):
// api/hello.ts
import type { VercelRequest, VercelResponse } from '@vercel/node'
export default function handler(req: VercelRequest, res: VercelResponse) {
const { name = 'World' } = req.query
res.status(200).json({ message: `Hello, ${name}!` })
}
CloudBase (cloudfunctions/hello/index.js):
// cloudfunctions/hello/index.js
exports.main = async (event, context) => {
const { name = 'World' } = event.queryStringParameters || {}
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello, ${name}!` }),
}
}
Tip: If you have more than 5 API routes, consider jumping to Scenario 2 and using CloudBase Run with Express for unified route management to avoid cloud function fragmentation.
Deployment Steps
# Step 1: Create cloud function directory
mkdir -p cloudfunctions/hello && cd cloudfunctions/hello
# Step 2: Initialize package.json (required for cloud functions)
npm init -y
# Step 3: Write the CloudBase version code above into index.js
# Step 4: Deploy
tcb fn deploy hello -e <envId>
# Step 5: Test invocation
tcb fn invoke hello -e <envId> --params '{"path":"/api/hello","httpMethod":"GET","queryStringParameters":{"name":"CloudBase"}}'
After deployment, check runtime status and logs at Cloud Functions → hello in the console.
2. Scenario 2: Multi-API Consolidation → CloudBase Run Express
When you have 5+ routes under
api/, splitting into separate cloud functions is expensive to manage. Use a single CloudBase Run service to host all APIs.
// cloudrun-app/index.js (Express example)
const express = require('express')
const app = express()
app.get('/api/hello', (req, res) => {
res.json({ message: `Hello, ${req.query.name || 'World'}!` })
})
app.get('/api/users/:id', (req, res) => {
res.json({ id: req.params.id })
})
app.post('/api/webhook', express.json(), (req, res) => {
console.log('webhook received:', req.body)
res.json({ ok: true })
})
app.listen(3000)
Deployment Steps
# Step 1: Prepare Dockerfile
# FROM node:20-alpine
# WORKDIR /app
# COPY package*.json ./
# RUN npm ci --only=production
# COPY . .
# EXPOSE 3000
# CMD ["node", "index.js"]
# Step 2: Deploy to CloudBase Run
tcb cloudrun deploy -e <envId> -s api --source ./cloudrun-app
# Step 3: Access the app
# https://<envId>-api.ap-shanghai.run.tcloudbase.com/api/hello
3. Scenario 3: SSR Frameworks → CloudBase Run
CloudBase Run: containerized deployment, supports any language/framework, long-lived connections (SSE/WebSocket), no timeout limit. Console: CloudBase Run.
Next.js SSR / ISR / API Routes
Applicable when: project depends on Next.js server-side capabilities (SSR, API Routes, Middleware, ISR)
Prepare Dockerfile:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY .next .next
COPY public public
COPY next.config.js .
EXPOSE 3000
CMD ["node_modules/.bin/next", "start"]
Build and deploy:
# Build Next.js production version
npx next build
# Deploy to CloudBase Run (container mode) using CLI
# Note: CLI only supports --source to specify code directory; CPU/memory/instance count must be configured in console service details
tcb cloudrun deploy \
-e <envId> \
-s nextjs-app \
--source .
API Routes handling:
Vercel's /api/* routes run as-is in CloudBase Run with no code changes needed:
// pages/api/hello.ts (no changes needed)
export default function handler(req, res) {
res.status(200).json({ message: 'Hello from CloudBase!' })
}
Deployment command summary:
The entire Next.js app is packaged as a Docker image and deployed to CloudBase Run. API Routes, SSR, and Middleware are all auto-hosted:
# Step 1: Build Next.js production version
npx next build
# Step 2: Prepare Dockerfile (refer to template above)
# Step 3: Deploy to CloudBase Run
tcb cloudrun deploy -e <envId> -s nextjs-app --source .
# Step 4: Access the app
# After deployment, check the access domain in console "CloudBase Run → nextjs-app"
Nuxt.js / SvelteKit / Astro SSR
Same pattern as Next.js: Dockerfile + CloudBase Run deployment.
# Step 1: Build production output (choose one per framework)
npx nuxi build # Nuxt
npm run build # SvelteKit
npm run build # Astro
# Step 2: Prepare Dockerfile (choose the corresponding CMD per framework)
# Nuxt: CMD ["node", ".output/server/index.mjs"]
# SvelteKit: CMD ["node", "build/index.js"]
# Astro: CMD ["node", "dist/server/entry.mjs"]
# Step 3: Deploy to CloudBase Run
tcb cloudrun deploy -e <envId> -s my-app --source .
# Step 4: Avoid cold starts
# Set MinNum to ≥ 1 in console "CloudBase Run → Service Configuration"
Module 3: Database
CloudBase offers three database types: PostgreSQL, MySQL (relational) + Document Database (NoSQL). Console: Database.
| Vercel Database | Type | CloudBase Equivalent | Notes |
|---|---|---|---|
| Marketplace · Neon (relational Postgres) | Integration | PostgreSQL Database / MySQL Database | Prefer PG for direct ecosystem compatibility |
| Marketplace · Supabase (full Postgres stack) | Integration | PostgreSQL Database | Includes Auth/Storage/Realtime; database-only migration possible |
| Vercel Edge Config (low-latency read-only config) | First-party | Document Database | Only suitable for infrequently written small data (feature flags, etc.) |
1. Relational Data Migration
Recommended: Marketplace · Neon (Postgres) → CloudBase PostgreSQL (same ecosystem, directly compatible)
Overall Flow
Local export backup.sql → Upload to Cloud Storage (get fileID) → Deploy Cloud Function → Trigger execution
Step 1 Step 2 Step 3 Step 4
Complete the import via Cloud Function + executePGSql, suitable for automation / large file batching.
Deployment Steps
Step 1: Export data locally (run locally, requires connectivity to Marketplace · Neon)
pg_dump --no-owner --no-acl --inserts postgres://<vercel-conn-str> > backup.sql
Step 2: Upload to Cloud Storage
Console Cloud Storage → Upload → get fileID (e.g. cloud://env-xxx/backup.sql).
Step 3: Deploy cloud function and configure environment variables
The cloud function requires TENCENTCLOUD_SECRETID / TENCENTCLOUD_SECRETKEY (console Cloud Functions → Function Configuration → Environment Variables).
mkdir -p cloudfunctions/pg-migrate && cd cloudfunctions/pg-migrate
npm init -y
npm i @cloudbase/manager-node
# Write the "Cloud Function Implementation" code below into index.js
tcb fn deploy pg-migrate -e <envId>
Step 4: Trigger execution (note: params passes fileID, not SQL content)
tcb fn invoke pg-migrate -e <envId> \
--params '{"envId":"<envId>","fileID":"cloud://<envId>/backup.sql"}'
Cloud Function Implementation
// cloudfunctions/pg-migrate/index.js
const CloudBase = require('@cloudbase/manager-node')
exports.main = async (event) => {
const { envId, fileID } = event
const app = CloudBase.init({
secretId: process.env.TENCENTCLOUD_SECRETID,
secretKey: process.env.TENCENTCLOUD_SECRETKEY,
envId
})
// 1. Download backup.sql from Cloud Storage
const { FileContent } = await app.storage.downloadFile({ fileID })
const sqlText = Buffer.from(FileContent).toString('utf-8')
// 2. Split SQL: remove comment lines + split by ;
// Note: $$ ... $$ function bodies and COPY ... FROM stdin are not suitable for this splitting
const stmts = sqlText
.split(/;\s*(?:\n|$)/)
.map(s => s.replace(/^--.*$/gm, '').trim())
.filter(Boolean)
// 3. Execute statements one by one, don't stop on failure (log errors)
let succeeded = 0
const errors = []
for (const sql of stmts) {
try {
await app.database.executePGSql({ Sql: sql + ';' })
succeeded++
} catch (e) {
errors.push({ sql: sql.slice(0, 200), message: e.message })
}
}
return { total: stmts.length, succeeded, failed: errors.length, errors }
}
Notes
executePGSqlreturnsColumns+Rows+AffectedRows, supports DDL / DML / DQL.- ⚠️ Limitation: When
pg_dumpoutput includesCREATE FUNCTION ... $$ ... $$;orCOPY ... FROM stdin;, the;-based SQL splitting logic may break. Consider using--insertsmode to export pure INSERT statements, or add$$block recognition to the script.
Module 4: File Migration
CloudBase Cloud Storage: built-in CDN domain, free HTTPS, image processing (thumbnails/crop/watermark), fine-grained security rules. Console: Storage → Cloud Storage.
CloudBase capability mapping: Vercel Blob is object storage; CloudBase's equivalent is Cloud Storage — with built-in CDN domain, free HTTPS, image processing (thumbnails/crop/watermark), and fine-grained security rules. The migration script for Blob files is provided below; code changes for existing upload/read operations mainly involve API replacement.
1. API Mapping
Vercel Blob (@vercel/blob) | CloudBase Cloud Storage (@cloudbase/js-sdk v3 / @cloudbase/manager-node) | Notes |
|---|---|---|
put(path, file) | app.uploadFile({ cloudPath, filePath }) | Direct path mapping (Vercel pathname → CloudBase cloudPath) |
del(url) | app.deleteFile({ fileList: [fileID] }) | CloudBase accepts cloud://... format fileID |
get(path) | app.getTempFileURL({ fileList: [{fileID, maxAge}] }) | Public read can get permanent links; private read must use temporary links |
list({ prefix }) | Not in Web/Node SDK; available in Manager Node SDK as storage.listDirectoryFiles(cloudPath[, limit, marker]) | On browser, maintain file list in your database; in Node.js scripts or Cloud Functions, use @cloudbase/manager-node; or manually export from console Storage → File Management |
head(path) | No native batch API; Manager Node SDK provides storage.getFileInfo(cloudPath) for single file metadata | For batch scenarios, maintain inventory in Document Database; for single file queries, use @cloudbase/manager-node |
access: 'public' / 'private' | Storage permissions (console "Cloud Storage → Permission Settings") | CloudBase uses directory-level permissions: public read / private read / various user identity reads |
2. Migration Script (Node.js)
Suitable for small to medium volumes (tens of thousands of files). Vercel Blob lists all pathnames via list, downloads each one, then uploads to CloudBase. Since CloudBase SDK does not provide listFiles, the flow is: Vercel list → CloudBase write.
// install: npm i @vercel/blob @cloudbase/js-sdk
const { list } = require('@vercel/blob')
const cloudbase = require('@cloudbase/js-sdk').default
const fs = require('fs')
const path = require('path')
const app = cloudbase.init({ env: 'your-env-id' })
const TMP_DIR = path.join(__dirname, '_blob_tmp')
async function migrateVercelBlob({ prefix = '', token }) {
let cursor
let total = 0
do {
const { blobs, hasMore, cursor: next } = await list({ prefix, cursor, token })
for (const blob of blobs) {
// 1. Download from Vercel Blob to local
const localPath = path.join(TMP_DIR, blob.pathname)
fs.mkdirSync(path.dirname(localPath), { recursive: true })
const res = await fetch(blob.url) // Vercel Blob URL is directly downloadable
const buf = Buffer.from(await res.arrayBuffer())
fs.writeFileSync(localPath, buf)
// 2. Upload to CloudBase Cloud Storage (cloudPath matches Vercel pathname)
const { fileID } = await app.uploadFile({
cloudPath: blob.pathname,
fileContent: fs.createReadStream(localPath),
})
console.log(`✓ ${blob.pathname} -> ${fileID}`)
total++
}
cursor = next
} while (cursor)
console.log(`Migration complete, ${total} files in total`)
}
migrateVercelBlob({ token: process.env.BLOB_READ_WRITE_TOKEN })
For large files (GB-level), it is recommended to first download a ZIP archive from Vercel console "Blob → Download All", then upload in batches to CloudBase console "Cloud Storage → Upload" to avoid script timeouts from prolonged execution.
3. Script Usage
This section describes three ways to run the §2 migration script. vercel-blob-migrate is both the directory name and the cloud function name for the §2 script (they share the same name); any reference to this name below refers to the same script. Choose any one of A / B / C based on data volume.
The migration script must run in an environment with access to both Vercel and CloudBase APIs. Running it inside CloudBase's own Cloud Functions / CloudBase Run is recommended — no maintenance, built-in logs, and no local bandwidth usage.
Method A: As CloudBase Cloud Function (⭐ Recommended, small to medium volume)
Suitable for tens of thousands of files. Cloud Functions have a 5-minute timeout limit; it is recommended to add "batch size + cursor continuation" logic to split large tasks into multiple invocations.
# 1. Create cloud function directory
mkdir -p cloudfunctions/vercel-blob-migrate && cd cloudfunctions/vercel-blob-migrate
# 2. Initialize and install dependencies
npm init -y
npm i @vercel/blob @cloudbase/js-sdk
# 3. Write the §2 script into index.js
# 4. Configure environment variables (console: Cloud Functions → Function Configuration)
# BLOB_READ_WRITE_TOKEN = <your-vercel-blob-token>
# 5. Deploy
tcb fn deploy vercel-blob-migrate -e <envId>
# 6. Trigger migration
tcb fn invoke vercel-blob-migrate -e <envId>
Method B: As CloudBase Run (large volume, hundred-thousand to million level)
No timeout limit. Wrap the script as an Express HTTP interface with paginated migration support:
// cloudrun-blob-migrate/index.js
const express = require('express')
const { list } = require('@vercel/blob')
const cloudbase = require('@cloudbase/js-sdk').default
const app = express()
const tcb = cloudbase.init({ env: process.env.CLOUDBASE_ENV_ID })
app.get('/migrate', async (req, res) => {
const { cursor: startCursor } = req.query
const { blobs, cursor: nextCursor } = await list({
cursor: startCursor || undefined,
token: process.env.BLOB_READ_WRITE_TOKEN,
})
for (const blob of blobs) {
const response = await fetch(blob.url)
const buf = Buffer.from(await response.arrayBuffer())
await tcb.uploadFile({ cloudPath: blob.pathname, fileContent: buf })
}
res.json({ migrated: blobs.length, nextCursor, done: !nextCursor })
})
app.listen(3000)
# Deploy
tcb cloudrun deploy -e <envId> -s blob-migrate --source ./cloudrun-blob-migrate
# Trigger (paginate: pass cursor for each call)
curl "https://<envId>-blob-migrate.ap-shanghai.run.tcloudbase.com/migrate"
curl "https://<envId>-blob-migrate.ap-shanghai.run.tcloudbase.com/migrate?cursor=<nextCursor>"
Method C: Local Execution (small batch, under 1,000 files)
# Install dependencies (current project)
npm i @vercel/blob @cloudbase/js-sdk
# Run
BLOB_READ_WRITE_TOKEN=<token> node migrate.js
4. Business Code Change Points
-
File ID format: Change from Vercel URL to CloudBase
cloud://format// Before (Vercel Blob)https://xxx.public.blob.vercel-storage.com/path/to/file.png// After (CloudBase)cloud://env-id.656e-env-id-1234567890/path/to/file.png -
Private read access: Use
getTempFileURLto get temporary HTTPS linksconst { fileList } = await app.getTempFileURL({fileList: [{ fileID: 'cloud://env-xxx/path/to/file.png', maxAge: 3600 }],})const url = fileList[0].tempFileURL -
Public read: Enable directory-level public read via cloud storage permission settings, then access using the permanent CDN domain
-
Client-side direct upload: Use
app.uploadFile()instead of@vercel/blob/client// Before (Vercel Blob client)import { upload } from '@vercel/blob/client'const blob = await upload('file.png', file, { access: 'public', handleUploadUrl: '/api/upload' })// After (CloudBase Web SDK)const { fileID } = await app.uploadFile({ cloudPath: 'file.png', filePath: file })
Module 5: Authentication
CloudBase has built-in authentication supporting five login sources: WeChat Open Platform, SMS, email/password, anonymous, and custom login tickets. Console: Identity Authentication.
Third-Party Auth Selection Guide for Vercel Ecosystem
| Solution | Positioning | Best For |
|---|---|---|
| NextAuth (Auth.js) | Open-source library, officially maintained by Next.js | Next.js projects (preferred) |
| Clerk | Hosted SaaS, out-of-box UI | When you need ready-made login UI |
| Auth0 | Enterprise-grade identity platform (Okta subsidiary) | Enterprise compliance, SSO |
All can be replaced with CloudBase built-in authentication after migration.
API Mapping
| Operation | Before (Vercel ecosystem) | After (CloudBase) |
|---|---|---|
| Get current user | useUser() / getServerSession() | (await auth.getSession()).data.session.user |
| Email/password login | Clerk/Auth0/NextAuth SDK | auth.signInWithPassword({ email, password }) |
| SMS verification code login | Third-party SMS gateway | auth.signInWithOtp({ phone }) → data.verifyOtp({ token }) (Shanghai region only) |
| Third-party OAuth | Clerk social login / Auth0 SSO | Custom login ticket + auth.signInWithCustomTicket(() => Promise.resolve(ticket)) |
| Logout | signOut() | auth.signOut() |
| Token / session | JWT / session cookie | (await auth.getSession()).data.session.access_token |
| User management (admin) | Clerk/Auth0 dashboard | CloudBase console / Node SDK auth.* |
Login Method Code Examples
Browser-side using @cloudbase/js-sdk:
import cloudbase from '@cloudbase/js-sdk'
const app = cloudbase.init({ env: 'your-env-id' })
const auth = app.auth // v3: `auth` is a property, not a method
// Anonymous login (suitable for quick prototyping)
await auth.signInAnonymously()
// SMS verification code login (Shanghai region only)
const { data } = await auth.signInWithOtp({ phone: '+8613800138000' })
await data.verifyOtp({ token: '123456' })
// Email + password login
await auth.signInWithPassword({ email: 'user@example.com', password: 'password' })
// WeChat Open Platform H5 login
await auth.signInWithWechatOpenPlatform({ appid: 'wx00...', scope: 'snsapi_base', redirectUri: 'https://...' })
// Custom login ticket (bridge for third-party OAuth, see below)
// Note: v3 signInWithCustomTicket takes a function, not an object
await auth.signInWithCustomTicket(() => Promise.resolve(ticket))
Existing User Migration
CloudBase does not provide a "batch import" console. You need to write an import-users cloud function to import users one by one.
Workflow:
- Export users from Clerk/Auth0/NextAuth as JSON/CSV:
- Clerk: Export docs
- Auth0: Export docs
- In the cloud function, call
auth.signUpWithEmailAndPassword()to write each user to CloudBase.
// cloudfunctions/import-users/index.js
const cloudbase = require('@cloudbase/js-sdk').default
// Inside Cloud Function, env auto-detected if not passed
const app = cloudbase.init({ env: process.env.CLOUDBASE_ENV_ID })
const users = require('./users.json') // Exported user data
exports.main = async () => {
const results = []
for (const user of users) {
try {
// @cloudbase/js-sdk v3 signUp method
await app.auth.signUpWithEmailAndPassword({
email: user.email,
password: '<temporary-password>',
})
results.push({ email: user.email, status: 'success' })
} catch (e) {
results.push({ email: user.email, status: 'failed', error: e.message })
}
}
return results
}
⚠️ Password hashes cannot be directly migrated; it is recommended to enforce a password reset on first login.
Third-Party OAuth Bridging
Bridge Google / GitHub / NextAuth or other third-party OAuth through the "custom login ticket" channel:
// cloudfunctions/create-ticket/index.js (server side)
const cloudbase = require('@cloudbase/js-sdk').default
const { SignJWT } = require('jose')
exports.main = async (event) => {
// 1. Verify third-party OAuth identity (e.g., verify Google token)
// const googleUser = await verifyGoogleToken(event.googleToken)
// 2. Issue CloudBase custom login ticket (HS256, 5 min expiry)
const app = cloudbase.init({ env: process.env.CLOUDBASE_ENV_ID })
const ticket = await app.auth().createTicket('google:12345', { refresh: 3600 * 1000 })
return { ticket }
}
// Browser side (v3: signInWithCustomTicket takes a function, not an object)
const ticketRes = await fetch('/api/create-ticket', { method: 'POST', body: JSON.stringify({ googleToken }) })
const { ticket } = await ticketRes.json()
await auth.signInWithCustomTicket(() => Promise.resolve(ticket))
Module 6: Environment Variables
| Dimension | Vercel | CloudBase |
|---|---|---|
| Configuration location | Dashboard "Settings → Environment Variables" / vercel env CLI | Console "Environment → Environment Variables" / tcb env:set CLI |
| Environments | Production / Preview / Development (per environment) | Per CloudBase environment |
| Code access | process.env.XXX | process.env.XXX (identical, no changes needed) |
| Local dev | vercel env pull | tcb env:list -e <envId> |
| Secrets encryption | Dashboard encrypted storage | Console encrypted storage |
process.env.XXXaccess in code requires no changes at all.
Migration Script Tools
Two CLI tools to help batch migrate Vercel project configuration:
| Script | Purpose | Usage |
|---|---|---|
migrate-vercel-config.js | Parse vercel.json, output CloudBase equivalent configuration | node migrate-vercel-config.js [vercel.json] (default ./vercel.json) |
batch-migrate-functions.js | Batch convert api/ Vercel Functions to Cloud Functions / CloudBase Run | node batch-migrate-functions.js ./api [output-dir] --target=function (Cloud Functions mode, default)node batch-migrate-functions.js ./api [output-dir] --target=cloudrun (CloudBase Run Express mode) |
End-to-End Migration Flow
# 0. Prerequisites: Install CLI and log in
npm install -g @cloudbase/cli
tcb login
# 1. Convert vercel.json configuration
node migrate-vercel-config.js
# 2. Batch generate cloud function skeleton (Cloud Functions mode)
node batch-migrate-functions.js ./api cloudfunctions --target=function
# 3. Deploy cloud functions one by one
for dir in cloudfunctions/*/; do
name=$(basename "$dir")
tcb fn deploy "$name" -e <envId>
done
# 4. Deploy static site
tcb app deploy -e <envId>
Script 1: migrate-vercel-config.js
#!/usr/bin/env node
// Parse vercel.json → output CloudBase equivalent configuration
const fs = require('fs')
const path = require('path')
const configPath = process.argv[2] || './vercel.json'
if (!fs.existsSync(configPath)) {
console.error(`✗ ${configPath} not found`)
process.exit(1)
}
const vercel = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
// 1. Static site hosting config (rewrites / redirects / headers)
const hosting = { rewrites: [], redirects: [], headers: [] }
if (vercel.rewrites) {
for (const r of vercel.rewrites) {
hosting.rewrites.push({
source: r.source,
destination: r.destination,
})
}
}
if (vercel.redirects) {
for (const r of vercel.redirects) {
hosting.redirects.push({
source: r.source,
destination: r.destination,
statusCode: r.permanent ? 301 : r.statusCode || 302,
})
}
}
if (vercel.headers) {
for (const h of vercel.headers) {
hosting.headers.push({
source: h.source,
headers: h.headers,
})
}
}
// 2. Functions mapping
const functions = vercel.functions || {}
const buildOutput = vercel.builds || []
// 3. Cron jobs
const crons = (vercel.crons || []).map(c => ({
path: c.path,
schedule: c.schedule,
}))
const result = JSON.stringify(
{
cloudbase: {
staticHosting: hosting,
functions: Object.keys(functions).length ? functions : '(no functions config)',
crons: crons.length ? crons : '(no crons)',
builds: buildOutput.map(b => ({ use: b.use, src: b.src })),
},
},
null,
2
)
console.log(result)
fs.writeFileSync('cloudbase-config.json', result)
console.log('\n✓ Output written to cloudbase-config.json')
Script 2: batch-migrate-functions.js
#!/usr/bin/env node
// Batch convert Vercel Functions → CloudBase Cloud Functions / CloudBase Run
const fs = require('fs')
const path = require('path')
const apiDir = process.argv[2] || './api'
const outputDir = process.argv[3] || './cloudfunctions'
const target = process.argv.find(a => a.startsWith('--target='))?.split('=')[1] || 'function'
if (!fs.existsSync(apiDir)) {
console.error(`✗ ${apiDir} not found`)
process.exit(1)
}
if (target === 'cloudrun') {
// CloudBase Run mode: generate an Express entry
const code = generateCloudRunEntry(apiDir)
fs.mkdirSync(outputDir, { recursive: true })
fs.writeFileSync(path.join(outputDir, 'index.js'), code)
fs.writeFileSync(
path.join(outputDir, 'package.json'),
JSON.stringify({ name: 'cloudrun-api', dependencies: { express: '^4.18.2' } }, null, 2)
)
console.log(`✓ CloudBase Run entry generated at ${outputDir}/index.js`)
process.exit(0)
}
// Cloud Functions mode: one function per file
fs.mkdirSync(outputDir, { recursive: true })
const files = fs.readdirSync(apiDir, { recursive: true }).filter(f => /\.[jt]s$/.test(f))
for (const file of files) {
const name = path.basename(file, path.extname(file)).replace(/^\[([^\]]+)\]$/, '_$1_')
const funcDir = path.join(outputDir, name)
fs.mkdirSync(funcDir, { recursive: true })
// Generate wrapper code
const wrapper = generateCloudFunctionWrapper(file)
fs.writeFileSync(path.join(funcDir, 'index.js'), wrapper)
fs.writeFileSync(
path.join(funcDir, 'package.json'),
JSON.stringify({ name: `fn-${name}`, main: 'index.js' }, null, 2)
)
console.log(`✓ Generated cloud function: ${name} (from ${file})`)
}
console.log(`\n✓ ${files.length} cloud function skeletons generated in ${outputDir}/`)
console.log(` Next: cd ${outputDir} && for d in */; do tcb fn deploy "$d" -e <envId>; done`)
// Helper function: generate cloud function wrapper
function generateCloudFunctionWrapper(filePath) {
const ext = path.extname(filePath)
const isTS = ext === '.ts'
const modulePath = path.join('../..', 'api', filePath.replace(ext, ''))
if (isTS) {
return `// CloudBase cloud function wrapper (TypeScript)
// Compile first: npx tsc || npx esbuild this-file --bundle --platform=node --outfile=index.cjs
// Or simply copy the handler logic directly into this file
exports.main = async (event, context) => {
const { default: handler } = await import('${modulePath}${ext}')
const response = await handler({
url: event.path,
method: event.httpMethod,
query: event.queryStringParameters || {},
body: event.body ? JSON.parse(event.body) : undefined,
headers: event.headers || {},
})
return {
statusCode: response.status || 200,
headers: response.headers || {},
body: JSON.stringify(response.body || response),
}
}
`
}
// JavaScript version
return `// CloudBase cloud function wrapper
// Automatically generated from: api/${filePath}
const handler = require('${modulePath}')
exports.main = async (event, context) => {
// Adapt event to Vercel handler signature
const req = {
url: event.path,
method: event.httpMethod,
query: event.queryStringParameters || {},
body: event.body ? JSON.parse(event.body) : undefined,
headers: event.headers || {},
}
const response = await handler(req)
return {
statusCode: response.statusCode || 200,
headers: { 'content-type': 'application/json', ...(response.headers || {}) },
body: JSON.stringify(response.body || response),
}
}
`
}
// Helper function: generate CloudBase Run entry (Express)
function generateCloudRunEntry(apiDir) {
const files = fs.readdirSync(apiDir, { recursive: true }).filter(f => /\.[jt]s$/.test(f))
let code = `// Auto-generated CloudBase Run Express entry (from Vercel Functions)
const express = require('express')
const app = express()
app.use(express.json())
`
for (const file of files) {
const route = '/' + file.replace(/\.(j|t)s$/, '').replace(/\[([^\]]+)\]/g, ':$1')
const modulePath = path.join('../..', 'api', file.replace(/\.(j|t)s$/, ''))
code += `app.all('${route}', async (req, res) => {
try {
const handler = require('${modulePath}')
const result = await handler(req)
res.status(result.statusCode || 200).json(result.body || result)
} catch (e) {
console.error('${route}:', e)
res.status(500).json({ error: e.message })
}
})
`
}
code += `
module.exports = app
// Direct run (for development use only; production uses start-server.js)
if (require.main === module) {
app.listen(3000, () => console.log('API server running on port 3000'))
}
`
return code
}
FAQ
1. SPA History Route 404
Set the static hosting error page to index.html:
tcb hosting config set -e <envId> --error-document index.html
Or configure in console: Static Website Hosting → Settings → Error Document → index.html.
2. Preview Deployments Alternative
Vercel Preview Deployments per Git branch can be replaced with multiple CloudBase environments (dev / staging / prod):
# Create environments
tcb env create dev # Development
tcb env create stage # Staging
# Deploy to respective environments
tcb app deploy -e dev
tcb app deploy -e stage
tcb app deploy -e prod
Add tcb app deploy -e <envId> to your CI/CD pipeline (GitHub Actions / GitLab CI) for the corresponding branch.
3. Timeout and Memory Comparison
| Vercel Functions | CloudBase Cloud Functions | CloudBase Run | |
|---|---|---|---|
| Timeout | 10s (Hobby) / 60s (Pro) / 900s (Enterprise) | Up to 900s | No timeout limit |
| Memory | 128MB - 3008MB | 256MB - 3072MB | Configurable per container spec |
| Concurrency | 1 per invocation | 1 per instance | Multiple per container |
4. Performance
- Domestic (China): CloudBase uses Tencent Cloud CDN for global acceleration; domestic access speed is significantly better than Vercel (which has no mainland China edge nodes).
- International: Both CloudBase and Vercel support CDN acceleration, with comparable performance.
- Cold starts: CloudBase Cloud Functions can be configured with
minNum >= 1to keep instances warm, eliminating cold starts for production applications.