老胡茶室
老胡茶室

出海产品的技术“黄金组合”:Next.js + Better-Auth + Polar + Resend 实战指南

冯宇

前言:扬帆出海

随着国内竞争加剧,越来越多开发者和创业者将目光投向了海外市场。“放眼海外”不再是大厂专属。小团队、独立开发者也能借助现代 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/uiTailwind 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 会尝试自动修复这些错误,最终生成一个像模像样的工程:

01

02

03

04

05

觉得还需要微调就继续对话,直到满意为止。然后我们就可以将生成的代码下载下来,在本地继续开发了。

说到扬帆出海,怎么能少了国际化支持呢?有了 AI 的帮助,我们可以轻松集成国际化支持。这次我们在本地使用 vscode 的 copilot 插件,直接输入:

给当前项目添加 i18n 支持,要求同时支持中英文两种语言,使用 `next-intl` 实现

第一次 copilot 虽然生成了代码,实现了功能,不过它的实现是将不同语言放在了不同的路径下,比如 /zh//en/,这并不是我想要的,我想要的是在同一个路径下根据用户的语言环境自动切换。于是进一步加入提示词,限定它的实现方式:

不要将不同的语言放到不同的 location (如 `/zh/`, `/en/` 等),所有的语言使用同样的 URL,应该自适应。如果用户第一次访问网站,默认应该使用用户浏览器语言,如果用户手动切换语言,则应该保存用户的语言设置,下次访问网站的时候使用用户的设置。语言切换按钮(地球图标)应该加入悬停或点击出现下拉框,提示当前网站支持的语言,下拉选项应该配上图标+文字,告诉用户点击选项将切换至哪种语言。

经过 Agent 一番吭哧吭哧的修改+BUG 修复,这次生成的效果基本符合预期了:

06

07

08

注意事项:

  • 为了防止 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 功能,可以方便的生成登录框的代码:

09

我们一样可以让 AI 直接帮我们生成登录页面的代码,比如我们可以输入:

在导航栏添加一个登录按钮,点击后弹出登录框,目前我们只支持 email OTP 方式登录,我们使用 better-auth 实现登录功能,已经添加了对应的后端代码。请生成一个美观的登录页面,使用 shadcn/ui 组件库,使用 Tailwind CSS 样式,支持国际化切换。

在纠正一些 AI 生成的代码不符合预期的细节之后,我们就可以得到一个美观的登录页面了,AI 还温馨的帮我们把国际化也一并做好了:

10

11

而且我们的邮件也是支持国际化的,AI 之前帮我们生成的邮件模板效果在这里也得到了体现:

12

13

登录之后的效果也有了,登录按钮会隐藏,显示用户头像和登出按钮:

14

Better-Auth 还支持其他多种社交平台登录方式,比如 GitHub、Google 等等,你可以根据需要添加对应的插件和配置。

集成 Polar.sh 实现支付与订单管理

到目前为止,我们已经完成了用户认证和邮件通知的集成,接下来我们将集成 Polar.sh 实现支付功能。

创建产品与定价

登录 Polar.sh ,创建产品与定价 (沙箱环境请登录 Polar.sh Sandbox):

  • 可配置订阅 (Subscription) 或一次性购买 (One-time Purchase)
  • 支持 License Key 自动下发

和传统的支付平台不同,polar.sh 专注于数字产品的销售,提供了完善的商品管理和订单管理系统,针对数字产品的销售,用户可以无需像传统的应用开发一样,自己开发订单管理系统。

我们像这样一个个上架自己的产品即可:

15

此时我们已经可以通过 Share 功能获取到产品的链接,分享给用户购买了,如无其他 UI 需求,我们甚至可以直接将这个链接放到我们的产品页面上,用户点击即可跳转至 Polar 支付页面完成购买。

16

17

当然如果希望将这个支付流程集成到我们的产品页面中,提供更好的用户体验,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",
});

18

19

点击后会跳转到 Polar 的支付界面 (沙箱环境支付卡号固定 4242 4242 4242 4242,其他随便填,只要在有效期内即可):

20

如果想获取做个管理界面,获取用户的订单列表,可以使用类似于这样的代码:

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。这个组合不仅可以帮助开发者快速上线产品,还能轻松实现全球支付、邮件通知和多语言支持。

同时,我们有成熟的出海产品代码模板,方便开发者快速构建自己的小生意,如有需要欢迎沟通交流。

付费内容

本文包含付费内容,需要会员权限才能查看完整内容。