rpc-api-contract

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

RPC / API contract

RPC / API 契约

How to write a business action that any channel — web, WhatsApp agent, MCP, cron, third party — can call without surprises. The same operation, one definition.
Why this exists (canon): the blocker to exposing our logic to agents was never "Postgres vs TypeScript" — it was the lack of a uniform contract (inconsistent naming, heterogeneous return types, no envelope, uneven idempotency). This skill is the operating procedure; the full rationale and the LaPyme benchmark live in the company brain page
estrategia/caso-lapyme
+
productos/profundizacion-tecnica/rpc-contract
. We assemble public conventions: Google AIP (resource-oriented + custom methods), RFC 7807/9457 (errors), Stripe / IETF Idempotency-Key, PostgREST api-schema.
如何编写业务动作,让任何渠道(web、WhatsApp agent、MCP、cron、第三方)都能无歧义地调用。同一操作,唯一定义。
制定背景(核心准则): 将我们的逻辑暴露给Agent的阻碍从来不是“选Postgres还是TypeScript”,而是缺少统一契约(命名不一致、返回类型不统一、无返回封装、幂等性实现参差不齐)。本规范是操作流程;完整的设计依据和LaPyme基准测试见公司知识库页面
estrategia/caso-lapyme
以及
productos/profundizacion-tecnica/rpc-contract
。我们整合了以下公开规范:Google AIP(面向资源 + 自定义方法)、RFC 7807/9457(错误格式)、Stripe / IETF Idempotency-KeyPostgREST api-schema。

When to apply

适用场景

  • Creating a new business action (a mutation or a non-trivial read meant to be called by a channel).
  • Exposing an existing function to a new channel (web → agent/MCP/third party).
  • Reviewing a migration that adds/changes a callable function.
Forward-only: applies to functions created from 2026-06-16. Legacy
rpc_*
/ unprefixed /
fn_
callables stay as they are until deliberately migrated. No mass retrofit — strangler fig (wrap on demand when a function gets exposed to a new channel).
  • 创建新的业务动作(供渠道调用的变更操作或非简单查询操作)。
  • 将现有函数暴露给新渠道(从web扩展到agent/MCP/第三方)。
  • 评审新增或修改可调用函数的数据库迁移。
仅向前适用: 仅对2026-06-16之后创建的函数生效。存量的
rpc_*
/ 无前缀 /
fn_
可调用函数保持原样,除非主动迁移。不进行批量改造 —— 采用绞杀者模式(当函数需要暴露给新渠道时按需封装)。

The architecture (where this fits)

架构定位(本规范所处层级)

   web UI  ·  WhatsApp agent  ·  MCP  ·  cron      ← channels (each a thin adapter)
                       │  all call the same core
   schema `api`  =  business actions  (this contract)   ← single definition
   public / private / <domain> schemas  =  internal logic, triggers, helpers
A channel (the WhatsApp agent, the web) brings its own LLM/UI. An MCP server for users is just the
api.*
actions wrapped in MCP protocol — no logic of its own (same model as LaPyme's
api.*
+
mcp.*
). Both bottom out on
api.*
.
   web UI  ·  WhatsApp agent  ·  MCP  ·  cron      ← channels (each a thin adapter)
                       │  all call the same core
   schema `api`  =  business actions  (this contract)   ← single definition
   public / private / <domain> schemas  =  internal logic, triggers, helpers
渠道(如WhatsApp agent、web端)自带各自的LLM/UI。面向用户的MCP服务器只是用MCP协议封装了
api.*
动作 —— 本身不包含业务逻辑(与LaPyme的
api.*
+
mcp.*
模式一致)。两者最终都调用
api.*

The 9 rules

9条规则

R1 — The boundary is the schema, not the prefix. Exposed actions live in a dedicated
api
schema (PostgREST serves one schema and generates its OpenAPI). Everything else is internal and not exposed. This is what makes the surface enumerable → OpenAPI → SDK + docs + MCP tools.
R2 — Naming (forward-only). Exposed:
api.<verb>_<noun>
(AIP style):
crear_
,
actualizar_
,
confirmar_
,
anular_
,
revertir_
,
listar_
,
obtener_
,
calcular_…_preview
. New internal:
fn_
. Legacy stays.
R3 — Success envelope, never
void
.
Every
api.*
returns
jsonb
:
json
{ "data":    { "...": "the affected resource(s): ids, amounts, state" },
  "effects": { "...": "what the command changed, grouped by domain" },
  "warnings":[ { "code": "STOCK_NEGATIVO", "message": "..." } ] }
data
never absent (use
{}
/
null
).
effects
groups side-effects by domain — its key taxonomy is per-project (see
references/<project>.md
); what's invariant is that
effects
exists. No
ok
/
success
flag — failure travels via the error (R4).
RETURNS void
is forbidden in
api.*
.
R4 — Errors, RFC 7807 style.
RAISE EXCEPTION
with a parseable, prefixed code:
RESOURCE_NOT_FOUND: educador % no existe
. The gateway maps it to
application/problem+json
(
{type, title, status, detail, code}
). Codes in
MAYUS_SNAKE
, domain-prefixed where it helps (
LIQUIDACION_PERIODO_CERRADO
).
R5 — Idempotency, explicit. Mutations accept
p_idempotency_key
and use
INSERT … ON CONFLICT
on a unique constraint — never a prior
IF EXISTS
(races under READ COMMITTED). Declare the state in the COMMENT:
[IDEMPOTENT: ON CONFLICT (...)]
or
[NOT IDEMPOTENT: caller guarantees single invocation]
.
R6 — Uniform auth.
SECURITY DEFINER
+
SET search_path = ''
+ an internal access check (
establecimiento_id
/
id_hub
) that does not depend on the caller's RLS. Identity is resolved server-side (closure / JWT) — never a caller-supplied argument an LLM could forge.
R7 — Documentation is mandatory.
COMMENT ON FUNCTION
on every
api.*
: what it does, its idempotency mark (R5), and which
effects
it produces. (This is what
db-reviewer
enforces and what auto-generates the docs.)
R8 — Preview/confirm for costly or irreversible ops. Expose a
…_preview
that returns the same
effects
shape
without committing (dry-run), plus a
confirmar_…
that executes.
R9 — Version without breaking. Don't mutate a live
api.*
contract. New signature →
_v2
, deprecate the old ≥ 2 cycles. Version the schema (
api
api_v1
) if needed.
R1 — 边界是schema,而非前缀。 对外暴露的动作存放在专用的**
api
** schema中(PostgREST服务单个schema并生成其OpenAPI)。其他所有内容都是内部的,不对外暴露。这使得暴露面可枚举 → 生成OpenAPI → 生成SDK + 文档 + MCP工具。
R2 — 命名规则(仅向前适用)。 对外暴露的函数命名为
api.<动词>_<名词>
(AIP风格):
crear_
actualizar_
confirmar_
anular_
revertir_
listar_
obtener_
calcular_…_preview
。新增的内部函数前缀为
fn_
。存量函数保持不变。
R3 — 成功返回封装,禁止返回
void
每个
api.*
函数都返回
jsonb
json
{ "data":    { "...": "the affected resource(s): ids, amounts, state" },
  "effects": { "...": "what the command changed, grouped by domain" },
  "warnings":[ { "code": "STOCK_NEGATIVO", "message": "..." } ] }
data
字段不可缺失(可使用
{}
/
null
)。
effects
按领域分组存放副作用 —— 其键的分类规则由各项目自行定义(见
references/<project>.md
);不变的要求是
effects
字段必须存在。不使用
ok
/
success
标志位 —— 失败通过错误返回(R4)。
api.*
中禁止使用
RETURNS void
R4 — 错误格式遵循RFC 7807。
RAISE EXCEPTION
时使用可解析的、带前缀的错误码:
RESOURCE_NOT_FOUND: educador % no existe
。网关会将其映射为
application/problem+json
格式(
{type, title, status, detail, code}
)。错误码使用
MAYUS_SNAKE
格式,必要时添加领域前缀(如
LIQUIDACION_PERIODO_CERRADO
)。
R5 — 幂等性显式声明。 变更操作接受
p_idempotency_key
参数,并在唯一约束上使用**
INSERT … ON CONFLICT
**实现幂等 —— 禁止先执行
IF EXISTS
判断(在READ COMMITTED隔离级别下会有竞态问题)。在函数COMMENT中声明幂等状态:
[IDEMPOTENT: ON CONFLICT (...)]
[NOT IDEMPOTENT: caller guarantees single invocation]
R6 — 统一鉴权。 使用
SECURITY DEFINER
+
SET search_path = ''
+ 内部访问校验
establecimiento_id
/
id_hub
),不依赖调用方的RLS。身份在服务端解析(闭包 / JWT)—— 绝对不能使用调用方传入的参数,因为LLM可能伪造该参数。
R7 — 强制要求文档。 每个
api.*
函数都必须添加
COMMENT ON FUNCTION
:说明函数用途、幂等性标记(R5)以及产生的
effects
。(这是
db-reviewer
会强制执行的检查项,也是自动生成文档的依据。)
R8 — 高成本或不可逆操作需提供预览/确认机制。 提供
…_preview
函数,以相同的
effects
结构
返回结果但不提交(试运行),同时提供
confirmar_…
函数执行实际操作。
R9 — 版本化不破坏兼容性。 不要修改正在使用的
api.*
契约。新增签名 → 加
_v2
后缀,旧版本至少保留2个周期后再废弃。必要时可对schema做版本化(
api
api_v1
)。

Template (new compliant action)

模板(符合规范的新增动作)

sql
create or replace function api.crear_<noun>(
  p_body jsonb,
  p_idempotency_key text
) returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_actor   <type> := <resolve from session, NOT from p_body>;  -- R6
  v_id      <type>;
  v_effects jsonb := '{}'::jsonb;
begin
  -- validate (read-only) → RAISE 'CODE: detalle' on failure (R4)

  insert into <domain>.<table> (...)
  values (...)
  on conflict (<idempotency unique key>) do nothing     -- R5
  returning id into v_id;

  if v_id is null then
    select id into v_id from <domain>.<table> where <idempotency key> = ...;
  end if;

  -- collect side-effects into v_effects, grouped by domain (R3)

  return jsonb_build_object(
    'data',     jsonb_build_object('<noun>_id', v_id, 'estado', '...'),
    'effects',  v_effects,
    'warnings', '[]'::jsonb
  );
end; $$;

comment on function api.crear_<noun>(jsonb, text) is
  'Crea <noun>. [IDEMPOTENT: ON CONFLICT (<key>)]. effects: {<dominios>}.';
sql
create or replace function api.crear_<noun>(
  p_body jsonb,
  p_idempotency_key text
) returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
  v_actor   <type> := <resolve from session, NOT from p_body>;  -- R6
  v_id      <type>;
  v_effects jsonb := '{}'::jsonb;
begin
  -- validate (read-only) → RAISE 'CODE: detalle' on failure (R4)

  insert into <domain>.<table> (...)
  values (...)
  on conflict (<idempotency unique key>) do nothing     -- R5
  returning id into v_id;

  if v_id is null then
    select id into v_id from <domain>.<table> where <idempotency key> = ...;
  end if;

  -- collect side-effects into v_effects, grouped by domain (R3)

  return jsonb_build_object(
    'data',     jsonb_build_object('<noun>_id', v_id, 'estado', '...'),
    'effects',  v_effects,
    'warnings', '[]'::jsonb
  );
end; $$;

comment on function api.crear_<noun>(jsonb, text) is
  'Crea <noun>. [IDEMPOTENT: ON CONFLICT (<key>)]. effects: {<dominios>}.';

Per-project specifics

各项目特有说明

The
effects
key taxonomy and the
api
schema bootstrap differ per repo. See
references/<project>.md
(e.g.
references/perennia-backoffice.md
,
references/gestionganadera.md
) for that repo's effect domains, identity resolution, and which legacy functions are already wrapped.
effects
的键分类规则和
api
schema的初始化方式因代码库而异。参见
references/<project>.md
(例如
references/perennia-backoffice.md
references/gestionganadera.md
)了解对应代码库的效果领域、身份解析方式以及已封装的存量函数列表。

Relationship to other skills

与其他规范的关系

  • db-reviewer
    enforces this on every migration (R1/R3/R5/R6/R7). If a check here isn't in db-reviewer yet, flag it.
  • supabase-postgres-best-practices
    covers the performance side (volatility, indexes).
  • Channels (WhatsApp agent, MCP) must call
    api.*
    — not tables directly, not generic CRUD.
  • db-reviewer
    会在每次数据库迁移时强制执行本规范(R1/R3/R5/R6/R7)。如果这里的检查项还未加入db-reviewer,请反馈。
  • supabase-postgres-best-practices
    涵盖性能相关的最佳实践(波动性、索引)。
  • 各渠道(WhatsApp agent、MCP)必须调用
    api.*
    —— 禁止直接访问表,也不能使用通用CRUD。