教程 / 搜索 / 后端集成 / 在灵码云上构建一个完整应用
📝 文字 ● 中级 更新于 2026-06-21

在灵码云上从头到尾构建一个完整应用

一句话:一个灵码云后端就是一个私有 Postgres 数据库(外加认证、存储、实时、函数、向量搜索),通过 HTTPS 访问。你在两个层里工作:schema 层(迁移、RLS、VIEW、RPC 函数——完整 SQL,由 agent 一次写好)和浏览器 SDK 层(按表 CRUD——不支持 JOIN、不支持 upsert,每页 200 行)。一个移动应用、一个 Web 前端、一个 Web API 都应该指向同一个后端、同一个 schema;你用行级安全(RLS)来隔离用户,而不是靠多开后端。用 IDE 的「云端」按钮托管 Web 构建产物(静态 → lingcode.dev/apps/<id>,或全栈 → <slug>.run.lingcode.dev)。先把约定理顺,agent 就能从一句 prompt 把整套东西搭出来。

大多数灵码云文档一次只讲一个产品——数据库认证托管。本指南反其道而行:它把发布一个真实应用的完整链路走一遍——后端、移动端 API、Web 前端、Web API——并回答那些页面没回答的问题:当多个客户端共用一个后端时,如何让它们的表、路由和数据不互相冲突?它是分层的——前半部分讲如何用 prompt 让 agent 来做;后半部分是当你想要更底层控制时的手写代码参考

你将学到

1. 灵码云是什么

当你登录后,你在 Mac IDE(或 /try)里打开的每个项目都能访问一个托管后端:一个由灵码替你运行的私有 Postgres 数据库,前面挡着一个 HTTPS 网关。没有要管理的连接串,也没有要运行的服务器。后端开箱即带:

在底层,每个后端都是一个隔离的 Postgres schema,带有自己的数据库角色。这种隔离是一道硬墙:一个后端在物理上读不到另一个后端的数据。记住这一点——它正是下面「一个后端、多个客户端」规则背后的原因。

2. 你构建时所处的两个层

这是全页最重要的概念。其余一切都由它推导而来。访问数据有两种方式,它们有意拥有不同的能力。

谁来写能做什么
Schema 层
(迁移、RLS、VIEW、RPC 函数、自定义函数)
agent(或你),通过迁移,一次写好 完整 Postgres。JOIN、CTE、聚合、upsert、索引、约束、生成列、触发器、服务端 SQL 函数。
浏览器 SDK
lingcode.from('table')…,运行在你发布的应用里)
你的应用代码,每次请求 仅按表 CRUD。不支持 JOIN、不支持 upsert,每次最多读 200 行。updatedelete 没有过滤器就拒绝执行。

心智模型

关系型和较重的工作放进 schema——定义一个 VIEW 或一个 rpc() 函数,写一次,然后像查普通表一样从浏览器读它。浏览器 SDK 只用于简单的读写。如果你发现自己想在应用代码里写 JOIN 或 upsert,那正是把它下沉一层、放进迁移的信号。

所以你从发布的应用里调用的数据 API 被有意做得小而安全。Postgres 的全部能力依然在——只是下沉到了一层,在你的 schema 里,由 agent 一次写好。本指南后续会不断依赖这个分层。

3. 一个后端,多个客户端

这是本指南存在的理由:你想要一个移动应用、一个 Web 前端,也许还有一些 Web API 路由。它们各自需要一个后端吗?

不需要。它们共用一个后端、一个 schema。在灵码上,「移动端 API」和「Web API」不是你架起来的两台独立服务器——它们是同一个网关和同一批表,只是被不同客户端访问:

让某个用户的数据保持私有的,不是给每个端开一个后端——而是行级安全(RLS)。每个登录用户拿到一个会话;网关把读写都钉到该用户的身份上;剩下的交给你的 RLS 策略。移动应用和 Web 应用对同一个用户看到的是完全相同的行,因为它们就是同一批行。

后端是硬隔离的——别不小心拆开。如果你为「移动端 API」另开一个后端,Web 应用的密钥读不到它,而且你无法跨这两个后端做 JOIN。它们是两个独立的数据库,你会被迫通过函数在它们之间来回搬数据。只有当它是一个真正独立、拥有自己用户群的产品时,才该开第二个后端——而不是为同一个应用的「另一个客户端」开。

当某个客户端确实需要另一个后端的数据(比如一个管理工具要读另一个产品),不要跨墙去拿——通过拥有方后端上的一个函数或 HTTP 端点把它暴露出来,再去调那个。墙保持完好,契约也清晰明确。

4. 命名约定(不冲突的规则)

因为每个客户端共用一个 schema,命名就是让它们互不踩脚的关键。这些都不是平台强制的——而是让多客户端应用保持清晰可读的约定。

该这样别这样
Web + 移动端共用一张 workouts 表,用 RLS 隔离每个客户端一张表(web_workoutsmobile_workouts
每张私有表都有 user_id + 一条 owner RLS 策略"我们到时候在应用代码里按用户过滤"
slug 形如 web-checkout / mobile-push两个函数都叫 handler
只为一个独立产品开第二个后端为同一应用的「移动版」开第二个后端

5. 如何给 agent 写 prompt

你很少手写 schema——你描述应用,让 agent 基于实时后端把它搭出来。它有直接用于创建后端、迁移和 CRUD 的工具,并且按固定顺序工作。一个好的 prompt 做三件事:列出表以及它们的归属、说明 RLS 意图、说明哪些逻辑必须在服务端。

让 agent 先做检查

后端的确切限制(套餐档位、函数超时、是否支持长时计算)是实时读取的,而不是假设的。一个可靠的习惯:在 prompt 开头加上"在设计前先检查后端能力",这样 agent 会调用 describe_backend,并按实际拥有的能力来设计——而不是去抓 localStorage 或臆造一个外部服务。

针对本指南正是这个场景——一个后端、Web + 移动端、不冲突——的 prompt:

在 LingCode Cloud 后端上构建一个健身应用。先检查后端能力,
然后按需创建后端并应用迁移。

一个后端、一个 schema,由一个 Web 应用和一个移动客户端共用。

表(snake_case、不加前缀):
  - workouts      (用户所属:user_id + owner RLS 策略)
  - goals         (用户所属:user_id + owner RLS 策略)
  - leaderboard   (公开可读,无 user_id;只由函数写入)

认证:邮箱/密码 + Google 登录。使用内置的 auth 用户表;
把 user_id 外键关联到它。

仅服务端逻辑(放进函数,而非浏览器):
  - billing-stripe-webhook   (校验并记录付款)
  - leaderboard-recompute    (定时任务;写入公开 leaderboard)

Web 前端直接用 SDK;移动客户端访问同一个网关。不要创建
多个后端或按客户端分表——用 RLS 隔离用户。为 feed 查询加索引,
并把 workout feed 暴露成一个 VIEW,让客户端无需 JOIN 即可读取。

在这个 prompt 背后,agent 遵循一个一致的顺序:

  1. 检查后端的能力和现有的表。
  2. 创建后端(如果项目还没有的话)——幂等,重复执行也安全。
  3. 应用迁移——CREATE TABLE、RLS 策略、索引、VIEW,以及任何 RPC 函数。
  4. 构建应用,用普通 CRUD 调用对接它,并创建服务端函数。

你也可以逐步显式驱动每一步——"加一张用户所属的 notifications 表,开启实时"——agent 会就地迁移 schema。schema 变更都是迁移,所以它们是有版本、可重复的,而不是一次性的改动。

6. 让你的项目可部署

后端是一半;托管应用是另一半。在 Mac IDE 里,工具栏上的「云端」按钮会构建你的项目并发布到一个真实的 HTTPS 地址。有两种托管层,具体走哪一种是从你的项目里检测出来的:

具体来说,让一个项目可部署需要:

边缘运行时,不是 Node。全栈应用以边缘 worker 的形式运行——一个 V8 isolate,而不是 Node 服务器——每个请求大约有 30 秒上限。长时任务(大批量导入、无头浏览器、跑好几分钟的作业)属于函数,而不是 API 路由。见下面的最佳实践。

7. 手写代码参考

如果你更想自己接 SDK——在你自己的 Next.js 项目、一个纯 HTML 页面或一个移动客户端里——这里是更底层的接口。(在 /try 和 Mac 预览里这些都已经以 window.lingcode 的形式预接好;只有把应用搬到别处时你才需要做这一步。专门的走查见在你自己的应用中使用 LingCode Cloud。)

创建一个客户端

你需要从 lingcode.dev/backends → 你的后端 → 连接详情里拿到两个公开值:数据 URL匿名密钥。匿名密钥本就该出现在浏览器里——访问控制由服务器端的 RLS 强制执行,而不是靠把密钥藏起来。

<script src="https://lingcode.dev/sdk/lingcode-v1.js"></script>
<script>
  const lingcode = LingCode.createClient(
    'https://lingcode.dev/api/cloud/be/<your-backend-id>',
    '<your-anon-key>'
  );
  await lingcode.ready;                 // 先处理完任何登录重定向
  const user = lingcode.auth.getUser(); // { id, email } | null
</script>

读写(CRUD 层)

// 读取——过滤器链接在动词之前;返回 { data, error }
const { data, error } = await lingcode
  .from('workouts')
  .eq('user_id', user.id)
  .order('created_at', { ascending: false })
  .limit(50)
  .select();

// 写入
await lingcode.from('workouts').insert({ title: 'Morning run', user_id: user.id });
await lingcode.from('workouts').eq('id', 1).update({ title: 'Evening run' }); // 必须有过滤器
await lingcode.from('workouts').eq('id', 1).delete();                          // 必须有过滤器

过滤运算符:.eq .neq .gt .gte .lt .lte .like .ilike .in(col,[…]) .is(col,null),以及 .match({a:1,b:2})。多个过滤器以 AND 连接。

认证、存储、实时、向量

处理任何关系型需求的逃生通道

浏览器 SDK 仅支持 CRUD,所以当你需要 JOIN、聚合或 upsert 时,把它推进 schema:创建一个 VIEW 并像查表一样从中 select(),或写一个 SQL 函数去调它。让 agent 在一个迁移里把它加上;然后你的应用代码保持简单,只读一张"表"。移动端和 Web 复用同一个视图——查询逻辑不重复。

8. 最佳实践与配额

下一步