Elysia with Supabase. Your next backend at sonic speed

Supabase 是 Firebase 的开源替代品,已成为开发者喜爱的工具包之一,以其快速开发而闻名。
它集成了 PostgreSQL、即用型用户认证、无服务器边缘函数、云存储等功能,一切就绪。
因为 Supabase 已经预构建并组合好了,您无需重复实现相同的功能——从第 100 次的相同特征减少到不到 10 行代码。
例如,认证功能,本来每个项目都需要重写上百行代码,现在只需:
supabase.auth.signUp(body)
supabase.auth.signInWithPassword(body)
然后 Supabase 将处理其余事项,通过发送确认链接确认电子邮件,或使用魔法链接或 OTP 进行认证,使用行级认证保护您的数据库,您可以想象得到。
那些在每个项目中需要花费数小时重复实现的事情,现在只需一分钟即可完成。
Elysia
如果您还没有听说过,Elysia 是一个以 Bun 为先的 Web 框架,注重速度和开发者体验。
Elysia 的性能比 Express 快近 20 倍,同时语法几乎与 Express 和 Fastify 相同。
(性能可能因机器而异,我们建议您在决定性能前在您的机器上运行 基准测试)
Elysia 拥有非常流畅的开发者体验。 不仅可以为类型定义单一真相来源,还能在您意外更改数据时检测并警告。
所有这些都通过简洁的声明式代码实现。
设置环境
您可以使用 Supabase Cloud 快速启动。
Supabase Cloud 将一键处理数据库设置、扩展以及云端所需的一切。

创建项目时,您应该会看到类似界面,填写所需信息,如果您在亚洲,Supabase 在新加坡和东京设有服务器。
(有时这对居住在亚洲的开发者来说是决定性因素,因为延迟问题)

创建项目后,您将看到欢迎屏幕,可以复制项目 URL 和服务角色。
两者都用于指示您在项目中使用的 Supabase 项目。
如果您错过了欢迎页,可以导航到 Settings > API,复制 Project URL 和 Project API keys。

现在在命令行中,您可以通过运行以下命令开始创建 Elysia 项目:
bun create elysia elysia-supabase
最后一个参数是 Bun 创建的文件夹名称,您可以随意更改名称。
现在,cd 进入该文件夹,因为我们要使用 Elysia 0.3 (RC) 中的新功能,首先需要安装 Elysia RC 通道,同时安装稍后将使用的 cookie 插件和 Supabase 客户端。
bun add elysia@rc @elysiajs/cookie@rc @supabase/supabase-js
让我们创建一个 .env 文件来加载 Supabase 服务密钥作为秘密。
# .env
supabase_url=https://********************.supabase.co
supabase_service_role=**** **** **** ****
您无需安装任何插件来加载 env 文件,因为 Bun 默认加载 .env 文件。
现在在您喜欢的 IDE 中打开项目,并在 src/libs/supabase.ts
内创建一个文件。
// src/libs/supabase.ts
import { createClient } from '@supabase/supabase-js'
const { supabase_url, supabase_service_role } = process.env
export const supabase = createClient(supabase_url!, supabase_service_role!)
就是这样!这就是设置 Supabase 和 Elysia 项目所需的一切。
现在让我们深入实现!
认证
现在让我们创建与主文件分离的认证路由。
在 src/modules/authen.ts
内,首先为我们的路由创建一个大纲。
// src/modules/authen.ts
import { Elysia } from 'elysia'
const authen = (app: Elysia) =>
app.group('/auth', (app) =>
app
.post('/sign-up', () => {
return 'This route is expected to sign up a user'
})
.post('/sign-in', () => {
return 'This route is expected to sign in a user'
})
)
现在,让我们应用 Supabase 来认证用户。
// src/modules/authen.ts
import { Elysia } from 'elysia'
import { supabase } from '../../libs'
const authen = (app: Elysia) =>
app.group('/auth', (app) =>
app
.post('/sign-up', async ({ body }) => {
const { data, error } = await supabase.auth.signUp(body)
if (error) return error
return data.user
return 'This route is expected to sign up a user'
})
.post('/sign-in', async ({ body }) => {
const { data, error } = await supabase.auth.signInWithPassword(
body
)
if (error) return error
return data.user
return 'This route is expected to sign in a user'
})
)
完成!这就是为用户创建 sign-in 和 sign-up 路由所需的一切。
但我们这里有一个小问题,您看,我们的路由可以接受 任何 body 并将其放入 Supabase 参数中,即使是无效的。
因此,为了确保我们放入正确的数据,我们可以为 body 定义一个 schema。
// src/modules/authen.ts
import { Elysia, t } from 'elysia'
import { supabase } from '../../libs'
const authen = (app: Elysia) =>
app.group('/auth', (app) =>
app
.post(
'/sign-up',
async ({ body }) => {
const { data, error } = await supabase.auth.signUp(body)
if (error) return error
return data.user
},
{
schema: {
body: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
}
}
)
.post(
'/sign-in',
async ({ body }) => {
const { data, error } =
await supabase.auth.signInWithPassword(body)
if (error) return error
return data.user
},
{
schema: {
body: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
}
}
)
)
现在我们在 sign-in 和 sign-up 中声明了 schema,Elysia 将确保传入的 body 与我们声明的形式相同,防止无效参数传入 supabase.auth
。
Elysia 也理解 schema,因此无需单独声明 TypeScript 类型,Elysia 会自动将 body
类型化为您定义的 schema。
因此,如果您在未来意外创建了破坏性更改,Elysia 将警告您数据类型。
我们拥有的代码很棒,它完成了我们期望的工作,但我们可以进一步提升它。
您看,sign-in 和 sign-up 都接受相同形状的数据,未来您可能还会发现自己在多个路由中重复长 schema。
我们可以通过告诉 Elysia 记住我们的 schema,然后通过告诉 Elysia 我们想要使用的 schema 名称来修复这个问题。
// src/modules/authen.ts
import { Elysia, t } from 'elysia'
import { supabase } from '../../libs'
const authen = (app: Elysia) =>
app.group('/auth', (app) =>
app
.setModel({
sign: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
})
.post(
'/sign-up',
async ({ body }) => {
const { data, error } = await supabase.auth.signUp(body)
if (error) return error
return data.user
},
{
schema: {
body: 'sign',
body: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
}
}
)
.post(
'/sign-in',
async ({ body }) => {
const { data, error } =
await supabase.auth.signInWithPassword(body)
if (error) return error
return data.user
},
{
schema: {
body: 'sign',
body: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
}
}
)
)
太好了!我们刚刚在路由上使用了名称引用!
TIP
如果您发现 schema 很长,可以将它们声明在单独的文件中,并在任何 Elysia 路由中重用,将焦点放回业务逻辑上。
存储用户会话
太好了,现在完成认证系统所需做的最后一件事是存储用户会话,在用户登录后,Supabase 中的令牌称为 access_token
和 refresh_token
。
access_token 是一个短期有效的 JWT 访问令牌。用于在短时间内认证用户。 refresh_token 是一个一次性使用的永不过期令牌,用于续期 access_token。因此,只要我们有这个令牌,就可以创建新的访问令牌来扩展用户会话。
我们可以将这两个值存储在 cookie 中。
现在,有些人可能不喜欢将访问令牌存储在 cookie 中的想法,并可能使用 Bearer。但为了简单起见,我们这里使用 cookie。
TIP
我们可以将 cookie 设置为 HttpOnly 以防止 XSS、Secure、Same-Site,还可以加密 cookie 以防止中间人攻击。
// src/modules/authen.ts
import { Elysia, t } from 'elysia'
import { cookie } from '@elysiajs/cookie'
import { supabase } from '../../libs'
const authen = (app: Elysia) =>
app.group('/auth', (app) =>
app
.use(
cookie({
httpOnly: true,
// If you need cookie to deliver via https only
// secure: true,
//
// If you need a cookie to be available for same-site only
// sameSite: "strict",
//
// If you want to encrypt a cookie
// signed: true,
// secret: process.env.COOKIE_SECRET,
})
)
.setModel({
sign: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
})
// rest of the code
)
这就是为 Elysia 和 Supabase 创建 sign-in 和 sign-up 路由所需的一切!

刷新令牌
现在,如前所述,access_token 是短期有效的,我们可能需要不时续期令牌。
幸运的是,我们可以使用 Supabase 的一行代码来实现。
// src/modules/authen.ts
import { Elysia, t } from 'elysia'
import { supabase } from '../../libs'
const authen = (app: Elysia) =>
app.group('/auth', (app) =>
app
.setModel({
sign: t.Object({
email: t.String({
format: 'email'
}),
password: t.String({
minLength: 8
})
})
})
.post(
'/sign-up',
async ({ body }) => {
const { data, error } = await supabase.auth.signUp(body)
if (error) return error
return data.user
},
{
schema: {
body: 'sign'
}
}
)
.post(
'/sign-in',
async ({ body }) => {
const { data, error } =
await supabase.auth.signInWithPassword(body)
if (error) return error
return data.user
},
{
schema: {
body: 'sign'
}
}
)
.get(
'/refresh',
async ({ setCookie, cookie: { refresh_token } }) => {
const { data, error } = await supabase.auth.refreshSession({
refresh_token
})
if (error) return error
setCookie('refresh_token', data.session!.refresh_token)
return data.user
}
)
)
最后,将路由添加到主服务器。
import { Elysia, t } from 'elysia'
import { auth } from './modules'
const app = new Elysia()
.use(auth)
.listen(3000)
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)
就是这样!
授权路由
我们刚刚实现了用户认证,这很有趣,但现在您可能发现自己需要为每个路由进行授权,并在各处重复检查 cookie 的代码。
幸运的是,我们可以在 Elysia 中重用函数。
让我们举个例子,假设我们希望用户创建一个简单的博客文章,其数据库 schema 如下:
在 Supabase 控制台中,我们将创建一个名为 'post' 的 Postgres 表,如下所示:
user_id 链接到 Supabase 生成的 auth 表作为 user.id,使用此关系,我们可以创建行级安全,只允许帖子所有者修改数据。

现在,让我们在另一个文件夹中创建一个新的独立 Elysia 路由,以将代码与 auth 路由分离,在 src/modules/post/index.ts
内。
// src/modules/post/index.ts
import { Elysia, t } from 'elysia'
import { supabase } from '../../libs'
export const post = (app: Elysia) =>
app.group('/post', (app) =>
app.put(
'/create',
async ({ body }) => {
const { data, error } = await supabase
.from('post')
.insert({
// Add user_id somehow
// user_id: userId,
...body
})
.select('id')
if (error) throw error
return data[0]
},
{
schema: {
body: t.Object({
detail: t.String()
})
}
}
)
)
现在,这个路由可以接受 body 并将其放入数据库,我们剩下要做的事就是处理授权并提取 user_id
。
幸运的是,我们可以轻松实现这一点,多亏了 Supabase 和我们的 cookie。
import { Elysia, t } from 'elysia'
import { cookie } from '@elysiajs/cookie'
import { supabase } from '../../libs'
export const post = (app: Elysia) =>
app.group('/post', (app) =>
app.put(
'/create',
async ({ body }) => {
let userId: string
const { data, error } = await supabase.auth.getUser(
access_token
)
if(error) {
const { data, error } = await supabase.auth.refreshSession({
refresh_token
})
if (error) throw error
userId = data.user!.id
}
const { data, error } = await supabase
.from('post')
.insert({
// Add user_id somehow
// user_id: userId,
...body
})
.select('id')
if (error) throw error
return data[0]
},
{
schema: {
body: t.Object({
detail: t.String()
})
}
}
)
)
太好了!现在我们可以使用 supabase.auth.getUser 从 cookie 中提取 user_id
。
Derive
我们的代码现在运行良好,但让我们描绘一个小场景。
假设您有许多需要授权的路由,如这样,需要提取 userId
,这意味着您将有很多重复代码,对吗?
幸运的是,Elysia 专门设计来解决这个问题。
在 Elysia 中,我们有称为 scope 的东西。
想象它就像一个 closure,其中变量仅在 scope 内可用,或者如果您来自 Rust,那就是所有权。
在 scope 中声明的任何生命周期,如 group、guard,仅在 scope 内可用。
这意味着您可以将特定生命周期声明应用于 scope 内的特定路由。
例如,需要授权的路由 scope,而其他的不需要。
因此,无需重用所有这些代码,我们只需定义一次并将其应用于您需要的路由。
现在,让我们将检索 user_id 移入一个插件,并将其应用于 scope 中的所有路由。
让我们将此插件放入 src/libs/authen.ts
。
import { Elysia } from 'elysia'
import { cookie } from '@elysiajs/cookie'
import { supabase } from './supabase'
export const authen = (app: Elysia) =>
app
.use(cookie())
.derive(
async ({ setCookie, cookie: { access_token, refresh_token } }) => {
const { data, error } = await supabase.auth.getUser(
access_token
)
if (data.user)
return {
userId: data.user.id
}
const { data: refreshed, error: refreshError } =
await supabase.auth.refreshSession({
refresh_token
})
if (refreshError) throw error
return {
userId: refreshed.user!.id
}
}
)
此代码尝试提取 userId,并将 userId
添加到路由的 Context
中,否则将抛出错误并跳过处理程序,防止无效错误进入我们的业务逻辑,即 supabase.from.insert。
TIP
我们也可以使用 onBeforeHandle 在进入主处理程序前创建自定义验证,.derive 也做同样的事,但从 derived 返回的任何内容将添加到 Context 中,而 onBeforeHandle 不做。
技术上,derive 在底层使用 transform。
只需一行,我们就将 scope 中的所有路由应用于仅授权路由,并具有类型安全的 userId 访问。
import { Elysia, t } from 'elysia'
import { authen, supabase } from '../../libs'
export const post = (app: Elysia) =>
app.group('/post', (app) =>
app
.use(authen)
.put(
'/create',
async ({ body, userId }) => {
let userId: string
const { data, error } = await supabase.auth.getUser(
access_token
)
if(error) {
const { data, error } = await supabase.auth.refreshSession({
refresh_token
})
if (error) throw error
userId = data.user!.id
}
const { data, error } = await supabase
.from('post')
.insert({
user_id: userId,
...body
})
.select('id')
if (error) throw error
return data[0]
},
{
schema: {
body: t.Object({
detail: t.String()
})
}
}
)
)
太好了,对吗?通过查看代码,我们甚至看不到我们处理了授权,就像魔法一样。
将我们的焦点放回核心业务逻辑上。

非授权 scope
现在让我们创建另一个路由从数据库中获取帖子。
import { Elysia, t } from 'elysia'
import { authen, supabase } from '../../libs'
export const post = (app: Elysia) =>
app.group('/post', (app) =>
app
.get('/:id', async ({ params: { id } }) => {
const { data, error } = await supabase
.from('post')
.select()
.eq('id', id)
if (error) return error
return {
success: !!data[0],
data: data[0] ?? null
}
})
.use(authen)
.put(
'/create',
async ({ body, userId }) => {
const { data, error } = await supabase
.from('post')
.insert({
// Add user_id somehow
// user_id: userId,
...body
})
.select('id')
if (error) throw error
return data[0]
},
{
schema: {
body: t.Object({
detail: t.String()
})
}
}
)
)
我们使用 success 来指示帖子是否存在。
如果不存在,我们将返回 success: false
和 data: null
。
如前所述,.use(authen)
应用于 scope 但 仅应用于其后声明的路由,这意味着之前的内容不受影响,而之后的内容现在是仅授权路由。
最后,别忘了将路由添加到主服务器。
import { Elysia, t } from 'elysia'
import { auth, post } from './modules'
const app = new Elysia()
.use(auth)
.use(post)
.listen(3000)
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)
奖励:文档
作为奖励,在我们创建的所有内容之后,无需逐路由解释,我们可以用 1 行代码为前端开发者创建文档。
使用 Swagger 插件,我们可以安装:
bun add @elysiajs/swagger@rc
然后只需添加插件:
import { Elysia, t } from 'elysia'
import { swagger } from '@elysiajs/swagger'
import { auth, post } from './modules'
const app = new Elysia()
.use(swagger())
.use(auth)
.use(post)
.listen(3000)
console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)
Tada 🎉 我们获得了良好定义的 API 文档。

如果有更多内容,您无需担心忘记 OpenAPI Schema 3.0 的规范,我们还有自动补全和类型安全。
我们可以使用 schema.detail
定义路由细节,它也遵循 OpenAPI Schema 3.0,因此您可以正确创建文档。
下一步
下一步,我们鼓励您尝试并探索本文中编写的 代码,并尝试添加图像上传帖子,以探索 Supabase 和 Elysia 生态系统。
如我们所见,使用 Supabase 创建生产就绪的 Web 服务器超级简单,许多事情只需一行代码,便于快速开发。
特别是与 Elysia 配对时,您将获得优秀的开发者体验、声明式 schema 作为单一真相来源,以及为创建 API 而精心设计的优秀选择、高性能服务器,同时使用 TypeScript,并且作为奖励,我们可以用一行代码创建文档。
Elysia 正在为创建以 Bun 为先的 Web 框架之旅中,采用新技术和新方法。
如果您对 Elysia 感兴趣,请随时查看我们的 Discord 服务器 或查看 Elysia on GitHub。
此外,您可能想查看 Elysia Eden,这是一个完全类型安全的、无代码生成的 fetch 客户端,类似于 tRPC,用于 Elysia 服务器。