用 Next.js 和 Supabase 在 2026 年构建 MemberPress 替代方案
用 Next.js 和 Supabase 构建 MemberPress 替代方案
我在 MemberPress 上构建过两次会员网站。两次我都在 18 个月内将其完全移除。这不是因为 MemberPress 不好——它确实是最好的 WordPress 会员插件之一——而是因为一旦你需要自定义功能,你就在与插件对抗,而不是在开发你的产品。
第三次,我用 Next.js 和 Supabase 从头开始构建了整个系统。核心功能只花了大约两周时间,结果更快、运行成本更低,并且比我用 WordPress 插件拼凑的任何东西都灵活无穷倍。如果你在 2026 年正在权衡 MemberPress 替代方案,让我为你节省一些时间:你不需要另一个插件。你需要一个你可以控制的技术栈。
本文详细介绍了如何构建生产就绪的会员网站——身份验证、基于角色的内容限制、Stripe 订阅、会员仪表板和管理工具——而无需接触 WordPress。
目录
- 为什么 MemberPress 对自定义项目不够
- 2026 年选择自定义技术栈的理由
- 架构概览:Next.js + Supabase + Stripe
- 为会员数据设置 Supabase
- 身份验证和基于角色的访问
- 使用 Next.js 中间件进行内容限制
- Stripe 订阅集成
- 构建会员仪表板
- 管理面板和分析
- 与 MemberPress 替代方案的比较
- 部署和成本
- 常见问题

为什么 MemberPress 对自定义项目不够
MemberPress 很适合特定用例:你有一个 WordPress 网站,你想在付费墙后面限制某些内容,并且你不需要超出插件提供的内容进行太多自定义。问题是大多数认真的会员业务很快就会超出这个框。
以下是我遇到的问题:
性能。 MemberPress 网站上的每次页面加载都要通过 WordPress 的 PHP 执行、会员资格检查的数据库查询,以及你堆积的任何其他插件。我的会员网站在共享主机上的 TTFB 达到 2-3 秒,即使在具有对象缓存的 VPS 上,它也很少降到 800ms 以下。
自定义上限。 MemberPress 为你提供了钩子和过滤器,但如果你想要自定义的入职流程、具有使用情况分析的个性化仪表板或根据会员进度动态调整的内容——你就在编写与插件架构对抗的自定义 PHP。
锁定。 你的会员数据、内容规则和业务逻辑都生存在 WordPress 的数据库模式中,与 MemberPress 的自定义表混淆。迁移出去并不简单。我做过。这是你不想经历的一个周末。
成本规模。 MemberPress Plus 每年运行 $399(2026 年定价)。加上可以处理认证流量的高级 WordPress 托管、缓存插件、安全插件和备份解决方案——你在支付 Stripe 交易费之前,基础设施上已经花费 $150-200/月。
这些都不意味着 MemberPress 不好。对于想要限制几篇博客文章并且不想写代码的独立创业者来说,它确实很好。但如果你正在构建一个核心产品的会员网站——尤其是如果你的团队中有开发人员——有更好的方法。
2026 年选择自定义技术栈的理由
工具环境发生了巨大的变化。在 2022 年,构建自定义会员网站意味着要连接一打服务并编写数千行样板代码。在 2026 年,三个工具给你提供了 MemberPress 所做的一切以及更多:
- Next.js 15 配合 App Router 处理渲染、路由、基于中间件的访问控制和 API 路由。
- Supabase 为你提供 Postgres 数据库、身份验证(包括魔法链接、OAuth 和电子邮件/密码)、行级安全和实时订阅——所有这些都有慷慨的免费层。
- Stripe 处理支付、订阅、发票、客户门户和税收合规性。
为服务 5,000 个会员的会员网站付费的总基础设施成本?大约 $25-45/月。我们稍后会分解数字。
架构概览:Next.js + Supabase + Stripe
以下是高级架构:
┌──────────────────────────────────────────────┐
│ Next.js 应用 │
│ ┌─────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 页面 │ │中间件 │ │ API 路由 │ │
│ │(限制+ │ │(认证+ │ │(webhooks+ │ │
│ │ 公开) │ │ RBAC) │ │ 管理 API) │ │
│ └────┬─────┘ └────┬─────┘ └──────┬────────┘ │
│ │ │ │ │
└───────┼────────────┼──────────────┼────────────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌─────▼─────┐
│Supabase │ │Supabase │ │ Stripe │
│ 数据库 │ │ 认证 │ │ 计费 │
└─────────┘ └─────────┘ └───────────┘
流程很直接:
- 用户注册 → Supabase Auth 创建用户
- 用户订阅 → Stripe Checkout 处理付款
- Stripe webhook → 更新 Supabase 中用户的订阅状态
- 用户访问限制内容 → Next.js 中间件检查他们在 Supabase 中的角色/等级
- 内容根据会员级别呈现或重定向

为会员数据设置 Supabase
从数据库模式开始。除了 Supabase Auth 现成提供的内容之外,你需要三个核心表:
-- 扩展 Supabase auth.users 的个人资料表
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
email text not null,
full_name text,
avatar_url text,
membership_tier text default 'free' check (membership_tier in ('free', 'basic', 'pro', 'enterprise')),
stripe_customer_id text unique,
subscription_status text default 'inactive' check (subscription_status in ('active', 'inactive', 'past_due', 'canceled')),
subscription_id text,
current_period_end timestamptz,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- 限制资源的内容表
create table public.content (
id uuid default gen_random_uuid() primary key,
title text not null,
slug text unique not null,
body text,
content_type text default 'article' check (content_type in ('article', 'video', 'download', 'course')),
required_tier text default 'free' check (required_tier in ('free', 'basic', 'pro', 'enterprise')),
published boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- 会员活动的审计日志
create table public.member_activity (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles on delete cascade,
action text not null,
metadata jsonb default '{}',
created_at timestamptz default now()
);
现在启用行级安全性。这是 Supabase 对会员网站真正闪耀的地方——数据库本身强制执行访问规则:
alter table public.profiles enable row level security;
alter table public.content enable row level security;
-- 用户可以阅读自己的个人资料
create policy "Users read own profile" on public.profiles
for select using (auth.uid() = id);
-- 用户可以更新自己的个人资料(但不能更新 membership_tier 或订阅字段)
create policy "Users update own profile" on public.profiles
for update using (auth.uid() = id)
with check (auth.uid() = id);
-- 基于会员等级的内容可见性
create or replace function public.tier_rank(tier text)
returns int as $$
begin
return case tier
when 'free' then 0
when 'basic' then 1
when 'pro' then 2
when 'enterprise' then 3
else 0
end;
end;
$$ language plpgsql security definer;
create policy "Members see content at or below their tier" on public.content
for select using (
published = true and (
required_tier = 'free'
or tier_rank(
(select membership_tier from public.profiles where id = auth.uid())
) >= tier_rank(required_tier)
)
);
设置触发器以在用户注册时自动创建个人资料:
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, email, full_name, avatar_url)
values (
new.id,
new.email,
new.raw_user_meta_data ->> 'full_name',
new.raw_user_meta_data ->> 'avatar_url'
);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
身份验证和基于角色的访问
Supabase 的 @supabase/ssr 包在 Next.js App Router 中处理认证。安装它:
npm install @supabase/supabase-js @supabase/ssr
为服务器组件创建一个 Supabase 客户端:
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
},
},
}
)
}
现在创建一个辅助函数来获取当前用户的会员数据:
// lib/membership.ts
import { createClient } from './supabase/server'
export type MembershipTier = 'free' | 'basic' | 'pro' | 'enterprise'
const TIER_HIERARCHY: Record<MembershipTier, number> = {
free: 0,
basic: 1,
pro: 2,
enterprise: 3,
}
export async function getMemberProfile() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null
const { data: profile } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single()
return profile
}
export function hasAccess(userTier: MembershipTier, requiredTier: MembershipTier): boolean {
return TIER_HIERARCHY[userTier] >= TIER_HIERARCHY[requiredTier]
}
使用 Next.js 中间件进行内容限制
这是魔法发生的地方。Next.js 中间件在页面呈现之前在边缘运行,所以未授权的用户甚至不会命中你的服务器组件:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { createServerClient } from '@supabase/ssr'
const PROTECTED_PATHS = [
{ path: '/members', requiredTier: 'basic' },
{ path: '/pro-content', requiredTier: 'pro' },
{ path: '/enterprise', requiredTier: 'enterprise' },
]
const TIER_RANK: Record<string, number> = {
free: 0, basic: 1, pro: 2, enterprise: 3,
}
export async function middleware(request: NextRequest) {
let response = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set(name, value)
response.cookies.set(name, value, options)
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
const pathname = request.nextUrl.pathname
const protectedRoute = PROTECTED_PATHS.find(p => pathname.startsWith(p.path))
if (protectedRoute) {
if (!user) {
return NextResponse.redirect(new URL('/login?redirect=' + pathname, request.url))
}
const { data: profile } = await supabase
.from('profiles')
.select('membership_tier, subscription_status')
.eq('id', user.id)
.single()
const userTier = profile?.membership_tier || 'free'
const isActive = profile?.subscription_status === 'active'
if (!isActive || TIER_RANK[userTier] < TIER_RANK[protectedRoute.requiredTier]) {
return NextResponse.redirect(new URL('/upgrade?required=' + protectedRoute.requiredTier, request.url))
}
}
return response
}
export const config = {
matcher: ['/members/:path*', '/pro-content/:path*', '/enterprise/:path*'],
}
这种方法比 MemberPress 基于 PHP 的内容限制要快得多。中间件在 CDN 边缘运行,所以延迟通常在 50ms 以下,无论你的用户身在何处。
Stripe 订阅集成
在 Stripe 中创建你的订阅产品,然后连接一个结账流程。这是 API 路由:
// app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const PRICE_MAP: Record<string, string> = {
basic: process.env.STRIPE_BASIC_PRICE_ID!,
pro: process.env.STRIPE_PRO_PRICE_ID!,
enterprise: process.env.STRIPE_ENTERPRISE_PRICE_ID!,
}
export async function POST(request: Request) {
const { tier } = await request.json()
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data: profile } = await supabase
.from('profiles')
.select('stripe_customer_id, email')
.eq('id', user.id)
.single()
let customerId = profile?.stripe_customer_id
if (!customerId) {
const customer = await stripe.customers.create({
email: profile?.email || user.email,
metadata: { supabase_user_id: user.id },
})
customerId = customer.id
await supabase
.from('profiles')
.update({ stripe_customer_id: customerId })
.eq('id', user.id)
}
const session = await stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: PRICE_MAP[tier], quantity: 1 }],
mode: 'subscription',
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/members/welcome?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: { supabase_user_id: user.id, tier },
})
return NextResponse.json({ url: session.url })
}
webhook 处理程序是关键部分——这是 Stripe 事件触发时更新 Supabase 数据库的方式:
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { createClient } from '@supabase/supabase-js'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // 管理员访问,绕过 RLS
)
export async function POST(request: Request) {
const body = await request.text()
const sig = request.headers.get('stripe-signature')!
const event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
)
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
const subscription = await stripe.subscriptions.retrieve(session.subscription as string)
const userId = session.metadata?.supabase_user_id
const tier = session.metadata?.tier
await supabaseAdmin.from('profiles').update({
membership_tier: tier,
subscription_status: 'active',
subscription_id: subscription.id,
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
}).eq('id', userId)
break
}
case 'customer.subscription.updated':
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
const customerId = subscription.customer as string
const { data: profile } = await supabaseAdmin
.from('profiles')
.select('id')
.eq('stripe_customer_id', customerId)
.single()
if (profile) {
await supabaseAdmin.from('profiles').update({
subscription_status: subscription.status === 'active' ? 'active' : 'inactive',
current_period_end: new Date(subscription.current_period_end * 1000).toISOString(),
}).eq('id', profile.id)
}
break
}
}
return NextResponse.json({ received: true })
}
构建会员仪表板
通过认证和计费连接,会员仪表板是从 Supabase 读取的标准服务器组件:
// app/members/dashboard/page.tsx
import { getMemberProfile } from '@/lib/membership'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
const profile = await getMemberProfile()
if (!profile) redirect('/login')
return (
<div className="max-w-4xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold mb-8">欢迎回来,{profile.full_name}</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-sm text-gray-500 uppercase">你的计划</h2>
<p className="text-2xl font-semibold capitalize mt-1">{profile.membership_tier}</p>
<p className="text-sm text-gray-500 mt-2">
续费于 {new Date(profile.current_period_end).toLocaleDateString()}
</p>
</div>
{/* 在此处添加更多仪表板小部件 */}
</div>
</div>
)
}
你还可以给会员访问 Stripe 客户门户的权限,以进行自助计费管理——不需要自定义计费 UI:
// app/api/billing-portal/route.ts
const session = await stripe.billingPortal.sessions.create({
customer: profile.stripe_customer_id,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/members/dashboard`,
})
管理面板和分析
对于管理仪表板,使用 Supabase 的服务角色密钥(仅限服务器端)来跨所有用户进行查询。你可以跟踪以下内容:
- 每天/周/月的新注册用户
- 按等级的流失率
- 收入(直接从 Stripe 的 API 拉取)
- 内容参与度(使用
member_activity表)
这是自定义构建真正付出回报的地方。MemberPress 为你提供了基本的统计页面。通过直接访问 Postgres 数据库,你可以运行任何你想要的查询。需要了解哪些文章能推动最多升级?将你的活动日志与你的个人资料更新连接。这种分析对于 SQL 来说很琐碎,对于 MemberPress 来说不可能,除非没有第三方分析工具。
与 MemberPress 替代方案的比较
让我们把这个与人们在 2026 年正在评估的流行替代方案进行对比:
| 功能 | MemberPress | Memberful | Paid Memberships Pro | 自定义(Next.js + Supabase) |
|---|---|---|---|---|
| 月费 | ~$33/月(年费) | $49/月 + 4.9% 交易费 | 免费(基础版)/ $347/年 | $25-45/月托管 |
| 交易费 | Stripe 标准 | 4.9% + Stripe | Stripe 标准 | 仅 Stripe 标准 |
| 自定义 UI | WordPress 主题 | 有限 | WordPress 主题 | 无限制 |
| 性能(TTFB) | 500ms-2s+ | ~200ms(托管) | 500ms-2s+ | <100ms(边缘) |
| 所需托管 | WordPress 托管 | 无(托管) | WordPress 托管 | Vercel/Netlify |
| 数据库访问 | WP + 插件表 | 无直接访问 | WP + 插件表 | 完整 Postgres 访问 |
| 内容类型 | 文章、页面、文件 | 文章、播客 | 文章、页面 | 任何你构建的 |
| API 访问 | 有限 REST | GraphQL API | 有限 REST | 完整 API 控制 |
| 供应商锁定 | 高(WP + 插件) | 中等 | 高(WP + 插件) | 低(标准工具) |
| 设置时间 | 1-2 小时 | 30 分钟 | 1-2 小时 | 1-2 周 |
| 最适合 | WP 内容限制 | 创作者、新闻通讯 | WP 电子商务 | 自定义产品 |
权衡是明确的:如果你想要一个标准的会员博客,Memberful 或 MemberPress 能让你更快地运行。但自定义路由为你提供了更好的性能、更低的持续成本(除了 Stripe 的 2.9% + 30¢ 之外没有平台交易费),以及对体验的完整控制。
如果你的团队没有一个熟悉 Next.js 的开发人员,这就是与专业的无头开发机构合作有意义的地方。我们在 Social Animal 已经在这个精确的技术栈上构建了几个会员平台——此处描述的架构本质上是我们的起始模板。
部署和成本
以下是为服务 5,000 个活跃会员的会员网站的现实成本分解:
| 服务 | 等级 | 月费 |
|---|---|---|
| Vercel(托管) | Pro | $20/月 |
| Supabase | Pro | $25/月 |
| Stripe | 按使用付费 | 每笔交易 2.9% + 30¢ |
| 域名 + DNS | Cloudflare | 免费 |
| 电子邮件(交易) | Resend | $20/月 |
| 总固定成本 | ~$65/月 |
将其与在优质 WordPress 托管(WP Engine 或 Kinsta 上运行 MemberPress 进行比较,成本为 ~$30-115/月)、加上插件许可证($399/年)、加上你需要的任何附加插件进行比较。自定义技术栈在价格上很有竞争力,性能上要好得多。
用 vercel --prod 部署到 Vercel。设置你的环境变量。配置 Stripe webhook 端点。你就上线了。
对于想要这个架构但不想自己维护它的团队,我们的无头 CMS 开发服务包括持续的维护和功能开发。我们还可以将 Supabase 与 Sanity 或 Payload 等无头 CMS 配对以处理内容管理层——如果你对静态优先方法感到好奇,我们的能力页面上有详细信息。
常见问题
用 Next.js 构建自定义会员网站比使用 MemberPress 更难吗? 说实话,是的——一开始。如果你是开发人员,期望花费约 1-2 周来构建核心:认证、计费、内容限制和会员仪表板。MemberPress 花费一个下午。区别在于启动后会发生什么。使用 MemberPress,每个自定义功能都是一场战斗。使用自定义技术栈,你是在一个你完全理解和控制的基础上构建。长期维护负担实际上更低,因为你不管理 WordPress 更新、插件冲突和十多个插件的安全补丁。
Supabase 的身份验证能力与 Auth0 等专业服务一样强吗? 对于会员网站,绝对可以。Supabase Auth 开箱即用支持电子邮件/密码、魔法链接、电话 OTP 和 OAuth 提供商(Google、GitHub、Apple 等)。它建立在 GoTrue 的基础上,这是 Netlify 使用的相同认证服务。对于 99% 的会员网站,它足有余了。你只有在有企业 SSO 需求(如 SAML)或复杂的多租户设置时才需要 Auth0。
没有 WordPress 我如何处理内容管理? 你有几个选择。你可以直接在 Supabase 中存储内容(对于较小的网站来说没问题),使用 Sanity、Payload 或 Contentful 等无头 CMS 来处理编辑体验,或者甚至为文档式会员网站在你的存储库中使用 MDX 文件。内容存储完全与会员逻辑解耦,这实际上是一个巨大的优势。
我如何处理滴水内容和预定发布?
向你的内容表添加 published_at 时间戳和 drip_days_after_signup 列。在你的查询中,比较会员的 created_at 日期加上滴水偏移与当前日期。这是单个 WHERE 子句。MemberPress 有一个专门的滴水功能,当然,但自定义版本给了你更多的灵活性——你可以根据课程进度、参与度指标或任何其他信号进行滴水。
与 WordPress 与 MemberPress 相比,这个方法的 SEO 如何? 在大多数情况下更好。Next.js 生成具有完整元数据控制的服务器呈现的 HTML。你获得更好的核心网络生命周期得分(直接影响 2026 年的排名)、对结构化数据的完整控制,以及向搜索引擎显示预告片内容的能力,同时限制完整版本。MemberPress 经常完全阻止搜索引擎访问内容,除非你仔细配置它。
我可以将我现有的 MemberPress 会员迁移到这个技术栈吗? 可以。从 MemberPress 导出你的会员(电子邮件、名称、订阅等级、Stripe 客户 ID)。编写一个迁移脚本来创建 Supabase Auth 用户和个人资料记录。由于大多数 MemberPress 网站使用 Stripe,你可以保留相同的 Stripe 客户 ID 和订阅——只需将 webhook 指向你的新端点。Stripe 订阅继续运行而不中断。
如果我需要社区功能,如论坛或评论,怎么办? Supabase 的实时订阅使构建实时评论系统或讨论论坛变得简单明了。对于更功能丰富的东西,与 Discord 集成(根据会员等级限制服务器访问)或嵌入 Hyvor Talk 之类的工具。要点是你选择适合的社区工具,而不是被锁定到 MemberPress 的附加组件生态系统提供的任何内容。
这个方法适合非技术创始人吗? 如果你不是开发人员,并且你的团队中也没有,这可能不是正确的路径。Memberful 是一个更好的选择——它是托管的,需要最少的设置,并与大多数网站平台集成。但如果你有一个开发人员(或你愿意雇用一个专门从事无头构建的机构),自定义方法将在你的会员业务增长时更好地为你服务。对于我们合作过的大多数项目,前期投资在 6-12 个月内就能收回成本。