前言:扬帆出海
随着国内竞争加剧,越来越多开发者和创业者将目光投向了海外市场。“放眼海外”不再是大厂专属。小团队、独立开发者也能借助现代 Web 技术实现产品快速落地、走向国际。
本文将介绍一个针对开发者出海的技术“黄金组合”:
- Next.js:现代 React 应用开发框架,支持 SSR/SSG,生态强大
- Better-Auth:新一代认证解决方案,开发体验优秀
- Polar.sh:专为数字产品设计的全球支付平台,支持银联卡。且对开发者极其友好。对 Next.js 和 Better-Auth 有很好的集成支持
- Resend:邮件发送服务,专注现代开发流程
Next.js
Next.js 是由 Vercel 推出的 React 应用框架,支持服务器端渲染(SSR)和静态生成(SSG),非常适合构建现代 Web 应用。它的生态系统强大,提供了丰富的插件和工具,能够帮助开发者快速构建高性能的应用。
Next.js 本身生态非常丰富,支持多种 UI 组件库和工具链,比如 shadcn/ui 与 Tailwind CSS 的无缝集成。它的 App Router 设计使得页面逻辑更加灵活,适合构建复杂的应用。无论自己开发还是国际协作开发,Next.js 都是一个非常好的选择,在全球范围享誉盛名,尤其在国外,使用 Next.js 的开发者和公司随处可见。
而且 Vercel 提供了商用支持和大量免费额度,特别适合初创团队和个人开发者,开发好的应用可以非常便捷的部署到 Vercel 平台上,向你的用户提供服务。
目前 AI 编程渐成主流,依托于 Next.js 的开发生态,AI 编程工具对其支持极其友好,生成的代码质量高,极大提升开发效率。
Better-Auth
Better-Auth 是一个新兴的认证解决方案,旨在简化现代 Web 应用的认证流程。支持主流 OAuth 平台如 GitHub、Google 等,同时也内置了 Email 登录功能。它提供了开箱即用的认证流程,开发者可以快速集成到 Next.js 应用中,同时提供了数据库模型设计的灵活性,方便用户自行管理用户数据。
相比 Auth.js,Better-Auth 提供了更简洁的接口和更清晰的逻辑,开发者体验更佳。它支持现代数据库模型设计,易于扩展和维护。
相比 Passport.js,Better-Auth 的配置和使用更加简单,内置了多种常用功能,减少了开发者的工作量。
相比其他第三方认证服务 (如 Clerk, Auth0 等等),Better-Auth 完全自行管理用户数据,提供了更高的灵活性和控制力,也避免了数据泄露的风险,以及减免了高昂的服务费用。
Polar.sh
Polar.sh 是一个专为数字产品设计的全球支付平台,支持电子产品(SaaS、文档、资源包等)的销售。它提供了完善的商品管理和订单管理系统,开发者无需额外开发后端逻辑即可实现支付功能。
Polar.sh 的开发者体验极佳,官方文档清晰,提供了沙箱环境供开发者测试。它支持全球信用卡、银联等多种支付方式,完美解决国内收款问题。
相比 Stripe,Polar.sh 支持电子商品管理和订单管理的功能,开发者可以无需额外开发订单系统即可实现支付功能,开发上更为简单。
相比 LemonSqueezy,Polar.sh 的定价策略更优,手续费更低,且支持更多支付方式。同时,开发体验上也不如 Polar.sh 友好。
需要特别注意的是,Polar.sh 的商品 (Product) 是没有库存管理功能的,因此只适用于无限库存的数字产品(如 SaaS、文档、资源包等,甚至支持 github private repo access)。大部分出海产品也都属于这一类。
有关 Polar.sh 支持的数字产品类型,可以参考官方 Blog
Resend
Resend 是一个现代邮件服务,专注于提供高效、易用的邮件发送解决方案。它支持自定义模板,能够满足各种邮件通知需求,如注册验证、密码重置等。Resend 支持使用自己的域名发送邮件,SDK 和 API 设计简洁,易用性相当好。
而且免费用户拥有每天 100 封邮件,每月 3000 封邮件的额度,适合初创团队和个人开发者使用。
我们将以一个实际项目为例,从工程初始化到认证、支付、邮件通知全流程,教你如何打造一个完整、可以线上收款的 MVP。
工程初始化
尽管我们可以按照 Next.js 的官方文档那样,从脚手架快速启动一个 Next.js 项目:
npx create-next-app@latest --typescript
但是 AI 时代的到来,我们显然有了更好的选择,就是直接利用 AI 帮我们生成一个 Next.js 项目的模板,并且做好基础页面,我们可以直接在此基础上进行开发就行了,节省大量的时间。
比如我们可以直接使用 v0 帮我们创建好现成的工程之后再开发。
v0 是 Vercel 推出的 AI 工具,天然对 Next.js 和 Vercel 平台有很好的支持,Next.js 用户绝对的首选,甚至它还支持生成的代码直接部署到 Vercel 平台上。
我们可以直接用自然语言描述我们的需求,添加我们对于技术栈的要求,让它生成现成的代码。比如我们想做一个卖个人知识付费的产品,面向出海,我们可以直接输入:
我想做一个卖个人数字产品的网站,使用 Next.js + TypeScript + shadcn/ui + Tailwind CSS + Postgresql + Drizzle ORM技术栈,使用 pnpm 管理项目依赖,所有的依赖请使用最新版本。你需要设计一个优美的界面,要有吸引人的文案,需要有网站首页和详情页。请生成一个 Next.js 项目模板,包含所有的依赖和配置。
然后 v0 就会自动生成一个 Next.js 项目模板,可能期间会有错误,不过 v0 会尝试自动修复这些错误,最终生成一个像模像样的工程:
觉得还需要微调就继续对话,直到满意为止。然后我们就可以将生成的代码下载下来,在本地继续开发了。
说到扬帆出海,怎么能少了国际化支持呢?有了 AI 的帮助,我们可以轻松集成国际化支持。这次我们在本地使用 vscode 的 copilot 插件,直接输入:
给当前项目添加 i18n 支持,要求同时支持中英文两种语言,使用 `next-intl` 实现
第一次 copilot 虽然生成了代码,实现了功能,不过它的实现是将不同语言放在了不同的路径下,比如 /zh/
和 /en/
,这并不是我想要的,我想要的是在同一个路径下根据用户的语言环境自动切换。于是进一步加入提示词,限定它的实现方式:
不要将不同的语言放到不同的 location (如 `/zh/`, `/en/` 等),所有的语言使用同样的 URL,应该自适应。如果用户第一次访问网站,默认应该使用用户浏览器语言,如果用户手动切换语言,则应该保存用户的语言设置,下次访问网站的时候使用用户的设置。语言切换按钮(地球图标)应该加入悬停或点击出现下拉框,提示当前网站支持的语言,下拉选项应该配上图标+文字,告诉用户点击选项将切换至哪种语言。
经过 Agent 一番吭哧吭哧的修改+BUG 修复,这次生成的效果基本符合预期了:
注意事项:
- 为了防止 AI 生成的代码不符合预期,甚至搞坏原本的代码无法恢复,我们强烈建议在修改代码前先使用 git 进行版本控制,随时 commit 一个可用的版本,方便 AI 生成的代码脱离掌控之后可以随时回滚到之前的版本。
- 从 v0 下载代码后务必注意下载后的代码目录结构是否跟 preview 界面中的代码一致。我遇到过明明 preview 展示的代码是 Next.js 推荐的新格式 (所有代码都在
src/
目录下),但是下载 zip 包中的代码却是混合结构(也就是src/
目录也有,根目录下也有),这种情况可能需要手动调整一下目录结构,删除和 preview 界面中不一致的目录,否则导致目录冲突,本地无法正确运行。 - 当前版本的 V0 无法预览
next-intl
国际化的代码,因此只能跳过,在本地单独编辑
集成 Better-Auth + Resend
接下来我们将集成 Better-Auth 实现用户认证,给我们的网站添加登录功能,并使用 Resend 实现邮件通知。
先配置好 Resend 邮件服务:
- 注册 Resend 账号:Resend 官网
- 想使用自己的域名发送邮件,需要先验证域名所有权
- Resend 可以使用
resend.dev
域名发送测试邮件,但是只能管理员本身收到,其他用户无法收到,特别注意。所以对外使用必须使用自己的域名。 - 获取 API Key,配置到项目中
项目中添加 Better-Auth 和 Resend 依赖:
pnpm add better-auth resend
better-auth 将数据库管理集成到你的项目中,我们已经使用了 drizzle-orm
作为 ORM,所以我们需要使用 Better-Auth 的 Drizzle ORM Adapter:
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
});
创建数据库模型:
npx -y @better-auth/cli generate
应该会生成一个 ./auth-schema.ts
文件,里面包含了 Better-Auth 的数据库模型定义。我们需要将这个文件放入 drizzle.config.ts
配置文件定义的 schema
文件或目录中。
然后运行 drizzle-kit
生成数据库迁移脚本并应用到数据库:
$ pnpm drizzle-kit generate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/home/fengyu/projects/digital-products-store/drizzle.config.ts'
4 tables
account 13 columns 0 indexes 1 fks
session 8 columns 0 indexes 1 fks
user 7 columns 0 indexes 0 fks
verification 6 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/0000_bent_mentor.sql 🚀
$ pnpm drizzle-kit migrate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/home/fengyu/projects/digital-products-store/drizzle.config.ts'
Using 'postgres' driver for database querying
[✓] migrations applied successfully!
TIPS:
- 我们建议系统的
user
表和 better auth 表分开,因为这样会更容易维护,避免过度耦合,提升系统的灵活性和可扩展性,这样我们无需对 better auth 的配置和表结构进行任何过度修改,使用也更容易。当 better auth 升级时,我们也无需过多修改原来的代码。 - better auth 的 generate 脚本生成的 schema 没有索引,你可以根据自己的实际需求,创建索引提升查询性能。如果你的表名发生冲突,可以考虑修改 better-auth 生成的 schema,将 better auth 的表放入单独的 postgresql schema 进行维护,不会影响到你的业务逻辑。
接下来我们添加 email 登录支持,使用 Resend 发送邮件:
// src/services/emailService.ts
// ...
export async function sendOTPEmail({
to,
otp,
locale = "zh",
}: SendOTPEmailParams) {
try {
const messages = getEmailMessages(locale);
const subject = messages.subject;
const html = generateOTPEmailHTML(otp, locale);
return sendHtml({
to,
subject,
html,
});
} catch (error) {
console.error("Failed to send OTP email:", error);
return false;
}
}
export async function sendHtml(params: {
to: string | string[];
subject: string;
html: string;
}) {
try {
const result = await resend.emails.send({
from: process.env.EMAIL_FROM ?? "no-reply@resend.dev",
to: params.to,
subject: params.subject,
html: params.html,
});
if (result.error) {
throw new Error(result.error.message);
}
return true;
} catch (error) {
console.error("Failed to send email:", error);
return false;
}
}
// src/lib/auth.js
// 继续添加 email OTP 登录支持
import type { Locale } from "@/i18n/config";
import { locales } from "@/i18n/config";
import { account, session, user, verification } from "@/schemas/db/auth-schema";
import { sendOTPEmail } from "@/services/emailService";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { emailOTP } from "better-auth/plugins";
import { cookies } from "next/headers";
import { db } from "./db";
async function getLocaleFromRequest(): Promise<Locale> {
try {
const cookieStore = await cookies();
const savedLocale = cookieStore.get("locale")?.value;
if (savedLocale && locales.includes(savedLocale as Locale)) {
return savedLocale as Locale;
}
return "zh";
} catch (error) {
console.warn("Failed to get locale from request:", error);
return "zh";
}
}
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: user,
session: session,
account: account,
verification: verification,
},
}),
plugins: [
emailOTP({
async sendVerificationOTP({ email, otp }) {
try {
const locale = await getLocaleFromRequest();
await sendOTPEmail({ to: email, otp, locale });
} catch (error) {
console.error("Failed to send OTP email:", error);
}
},
}),
],
});
然后添加 better auth 集成 Next.js 的配置:
// src/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
// src/lib/auth-client.ts
import { emailOTPClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
plugins: [emailOTPClient()],
});
export const { signIn, signOut, useSession } = authClient;
如果需要在中间件做一些拦截处理的话,可以参照官方文档的示例,在中间件添加额外的逻辑。至此后端逻辑已经完成。
接下来我们添加登录页面,并处理用户状态。better-auth 官方在首页提供了一个方便的 Create Sign in Box
功能,可以方便的生成登录框的代码:
我们一样可以让 AI 直接帮我们生成登录页面的代码,比如我们可以输入:
在导航栏添加一个登录按钮,点击后弹出登录框,目前我们只支持 email OTP 方式登录,我们使用 better-auth 实现登录功能,已经添加了对应的后端代码。请生成一个美观的登录页面,使用 shadcn/ui 组件库,使用 Tailwind CSS 样式,支持国际化切换。
在纠正一些 AI 生成的代码不符合预期的细节之后,我们就可以得到一个美观的登录页面了,AI 还温馨的帮我们把国际化也一并做好了:
而且我们的邮件也是支持国际化的,AI 之前帮我们生成的邮件模板效果在这里也得到了体现:
登录之后的效果也有了,登录按钮会隐藏,显示用户头像和登出按钮:
Better-Auth 还支持其他多种社交平台登录方式,比如 GitHub、Google 等等,你可以根据需要添加对应的插件和配置。
集成 Polar.sh 实现支付与订单管理
到目前为止,我们已经完成了用户认证和邮件通知的集成,接下来我们将集成 Polar.sh 实现支付功能。
创建产品与定价
登录 Polar.sh ,创建产品与定价 (沙箱环境请登录 Polar.sh Sandbox):
- 可配置订阅 (Subscription) 或一次性购买 (One-time Purchase)
- 支持 License Key 自动下发
和传统的支付平台不同,polar.sh 专注于数字产品的销售,提供了完善的商品管理和订单管理系统,针对数字产品的销售,用户可以无需像传统的应用开发一样,自己开发订单管理系统。
我们像这样一个个上架自己的产品即可:
此时我们已经可以通过 Share
功能获取到产品的链接,分享给用户购买了,如无其他 UI 需求,我们甚至可以直接将这个链接放到我们的产品页面上,用户点击即可跳转至 Polar 支付页面完成购买。
当然如果希望将这个支付流程集成到我们的产品页面中,提供更好的用户体验,Polar.sh 也提供了完善的 API 和 SDK,方便我们使用。
注意: 当前 Polar.sh 只支持美元定价,暂不支持本国货币。所有的支付会统一换算成美元之后进行结算。
集成支付流程
Polar.sh 直接支持 Better-Auth 或者 Next.js 的集成。这里我们使用和 Better-Auth 集成的方案:
pnpm add @polar-sh/better-auth @polar-sh/sdk
在 .env 中添加 Polar 的配置:
POLAR_ACCESS_TOKEN=...
# 如果你需要使用 Polar 的 Webhook 功能,需要配置 Webhook Secret
POLAR_WEBHOOK_SECRET=...
然后我们在 Better-Auth 的配置中添加 Polar 插件相关配置:
// src/lib/auth.ts
import { checkout, polar, portal, webhooks } from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";
const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server:
(process.env.POLAR_SERVER as "sandbox" | "production" | undefined) ??
"sandbox",
});
export const auth = betterAuth({
// ...
plugins: [
// ...
polar({
client: polarClient,
createCustomerOnSignUp: true,
use: [
checkout({
products: [
{
productId:
"这里替换成你的 Product ID,可以在 Polar.sh 的的 Products 页面找到",
slug: "1",
},
// 其他产品可以继续添加
],
successUrl: "/",
authenticatedUsersOnly: true,
}),
portal(),
webhooks({
secret: process.env.POLAR_WEBHOOK_SECRET ?? "",
onPayload: async (payload) => {
console.log("payload", payload);
},
}),
],
}),
],
});
// src/lib/auth-client.ts
// 追加 Polar 插件
import { createAuthClient } from "better-auth/react";
import { polarClient } from "@polar-sh/better-auth";
import { organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [polarClient()],
});
商品管理这里我们有全自动的“产品即代码” migration 解决方案,在发布时自动同步商品到 Polar.sh。有兴趣的话,可以在评论区留言,或者给作者发私信。
这样我们就可以在用户点击支付按钮时,通过使用方法:
await authClient.checkout({
// Any Polar Product ID can be passed here
products: ["PRODUCT ID"],
// Or, if you setup "products" in the Checkout Config, you can pass the slug
slug: "YOUR SLUG",
});
点击后会跳转到 Polar 的支付界面 (沙箱环境支付卡号固定 4242 4242 4242 4242
,其他随便填,只要在有效期内即可):
如果想获取做个管理界面,获取用户的订单列表,可以使用类似于这样的代码:
const { data: orders } = await authClient.customer.orders.list({
query: {
page: 1,
limit: 100,
productBillingType: "one_time", // 一次性购买
},
});
这样就可以实现完全托管的支付流程了,对于简单的数字产品的销售与管理,我们完全不需要自己开发任何支付和订单逻辑,Polar.sh 已经帮我们实现了所有的支付流程和订单管理。
退订
对于订阅 (subscription) 产品,Polar.sh 也提供了完善的退订流程。
Polar.sh 和 Better Auth 集成后,对于成功支付的用户会自动创建 Polar.sh 账户,这样用户可以在 Polar.sh 个人中心直接管理自己的订阅,包括取消订阅、修改订阅等。
可以通过代码跳转到 Polar 的个人中心:
await authClient.customer.portal();
小结
通过本文的介绍,我们实现了 Next.js + Better-Auth + Polar + Resend 技术组合,快速构建了一个出海数字产品的 MVP。这个组合不仅可以帮助开发者快速上线产品,还能轻松实现全球支付、邮件通知和多语言支持。
同时,我们有成熟的出海产品代码模板,方便开发者快速构建自己的小生意,如有需要欢迎沟通交流。
本文包含付费内容,需要会员权限才能查看完整内容。