一句话:一个灵码云后端就是一个私有 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 来做;后半部分是当你想要更底层控制时的手写代码参考。
当你登录后,你在 Mac IDE(或 /try)里打开的每个项目都能访问一个托管后端:一个由灵码替你运行的私有 Postgres 数据库,前面挡着一个 HTTPS 网关。没有要管理的连接串,也没有要运行的服务器。后端开箱即带:
在底层,每个后端都是一个隔离的 Postgres schema,带有自己的数据库角色。这种隔离是一道硬墙:一个后端在物理上读不到另一个后端的数据。记住这一点——它正是下面「一个后端、多个客户端」规则背后的原因。
这是全页最重要的概念。其余一切都由它推导而来。访问数据有两种方式,它们有意拥有不同的能力。
| 层 | 谁来写 | 能做什么 |
|---|---|---|
| Schema 层 (迁移、RLS、VIEW、RPC 函数、自定义函数) |
agent(或你),通过迁移,一次写好 | 完整 Postgres。JOIN、CTE、聚合、upsert、索引、约束、生成列、触发器、服务端 SQL 函数。 |
| 浏览器 SDK ( lingcode.from('table')…,运行在你发布的应用里) |
你的应用代码,每次请求 | 仅按表 CRUD。不支持 JOIN、不支持 upsert,每次最多读 200 行。update 和 delete 没有过滤器就拒绝执行。 |
把关系型和较重的工作放进 schema——定义一个 VIEW 或一个 rpc() 函数,写一次,然后像查普通表一样从浏览器读它。浏览器 SDK 只用于简单的读写。如果你发现自己想在应用代码里写 JOIN 或 upsert,那正是把它下沉一层、放进迁移的信号。
所以你从发布的应用里调用的数据 API 被有意做得小而安全。Postgres 的全部能力依然在——只是下沉到了一层,在你的 schema 里,由 agent 一次写好。本指南后续会不断依赖这个分层。
这是本指南存在的理由:你想要一个移动应用、一个 Web 前端,也许还有一些 Web API 路由。它们各自需要一个后端吗?
不需要。它们共用一个后端、一个 schema。在灵码上,「移动端 API」和「Web API」不是你架起来的两台独立服务器——它们是同一个网关和同一批表,只是被不同客户端访问:
fetch 访问同一个数据 URL)。让某个用户的数据保持私有的,不是给每个端开一个后端——而是行级安全(RLS)。每个登录用户拿到一个会话;网关把读写都钉到该用户的身份上;剩下的交给你的 RLS 策略。移动应用和 Web 应用对同一个用户看到的是完全相同的行,因为它们就是同一批行。
当某个客户端确实需要另一个后端的数据(比如一个管理工具要读另一个产品),不要跨墙去拿——通过拥有方后端上的一个函数或 HTTP 端点把它暴露出来,再去调那个。墙保持完好,契约也清晰明确。
因为每个客户端共用一个 schema,命名就是让它们互不踩脚的关键。这些都不是平台强制的——而是让多客户端应用保持清晰可读的约定。
snake_case、对应用有意义、不加前缀。把表命名为 workouts,而不是 mobile_workouts 或 app_v2_workouts。后端本就活在自己私有的 schema 里,所以你永远不用自己加后端前缀——那是替你生成的。一张表服务所有客户端。user_id 和一条 RLS 策略。这正是让 Web 和移动端安全共用一张表的机制:
CREATE TABLE workouts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth_users(id) ON DELETE CASCADE,
title text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE workouts ENABLE ROW LEVEL SECURITY;
CREATE POLICY workouts_owner ON workouts
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
user_id。一个人人可读的 product_catalog 或 leaderboard,策略允许所有人读、不允许任何人写(写入在服务端由函数完成)。web-checkout、mobile-push-register、billing-stripe-webhook。slug 就是 URL,清晰的 slug 就是清晰的端点。workout_feed、get_leaderboard(limit int)。多个客户端复用同一个,而不是各自发明一套。| 该这样 | 别这样 |
|---|---|
Web + 移动端共用一张 workouts 表,用 RLS 隔离 | 每个客户端一张表(web_workouts、mobile_workouts) |
每张私有表都有 user_id + 一条 owner RLS 策略 | "我们到时候在应用代码里按用户过滤" |
slug 形如 web-checkout / mobile-push | 两个函数都叫 handler |
| 只为一个独立产品开第二个后端 | 为同一应用的「移动版」开第二个后端 |
你很少手写 schema——你描述应用,让 agent 基于实时后端把它搭出来。它有直接用于创建后端、迁移和 CRUD 的工具,并且按固定顺序工作。一个好的 prompt 做三件事:列出表以及它们的归属、说明 RLS 意图、说明哪些逻辑必须在服务端。
后端的确切限制(套餐档位、函数超时、是否支持长时计算)是实时读取的,而不是假设的。一个可靠的习惯:在 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 遵循一个一致的顺序:
CREATE TABLE、RLS 策略、索引、VIEW,以及任何 RPC 函数。你也可以逐步显式驱动每一步——"加一张用户所属的 notifications 表,开启实时"——agent 会就地迁移 schema。schema 变更都是迁移,所以它们是有版本、可重复的,而不是一次性的改动。
后端是一半;托管应用是另一半。在 Mac IDE 里,工具栏上的「云端」按钮会构建你的项目并发布到一个真实的 HTTPS 地址。有两种托管层,具体走哪一种是从你的项目里检测出来的:
index.html)。发布到 https://lingcode.dev/apps/<id>。覆盖 Vite/React/Vue/Svelte 单页应用和纯静态站点。package.json 自动检测:Next.js、SvelteKit、Nuxt、Astro、TanStack Start 或 React Router 7。发布到 <slug>.run.lingcode.dev,运行在边缘。你的 Web API 路由也住在这里。具体来说,让一个项目可部署需要:
web/、frontend/、apps/、packages/ 这些惯用位置。把 Web 应用放在其中之一能让检测保持自动。如果你更想自己接 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>
// 读取——过滤器链接在动词之前;返回 { 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 连接。
auth.signUp({email,password}) / signIn(...);免密码 sendMagicLink({email});验证码 sendOtp + verifyOtp;社交登录先 getProviders() 再 signInWithOAuth('google')。SDK 会存储会话并附加到之后的调用上,所以读取以登录用户身份运行,RLS 据其 id 生效。await lingcode.storage.from('public').upload('avatars/me.png', file) → data.url;getPublicUrl(path);download(path)。const off = lingcode.from('workouts').subscribe(({type,row}) => { /* INSERT|UPDATE|DELETE */ });调用 off() 停止。事件经过 RLS 过滤。用它取代轮询。embedding 列,用 lingcode.vector.search({ table, column, embedding, limit, metric:'cosine' }) 按相似度排序。lingcode.functions.invoke('web-checkout', input) 运行你的服务端函数(这是用私密密钥的地方)。浏览器 SDK 仅支持 CRUD,所以当你需要 JOIN、聚合或 upsert 时,把它推进 schema:创建一个 VIEW 并像查表一样从中 select(),或写一个 SQL 函数去调它。让 agent 在一个迁移里把它加上;然后你的应用代码保持简单,只读一张"表"。移动端和 Web 复用同一个视图——查询逻辑不重复。