Traducido del original en chino. Si ambos difieren, el original prevalece.
Confer — Modelo de datos
Esquema de tablas de PostgreSQL y definiciones de tipos de TypeScript. Todos los identificadores son ULID (ordenados por tiempo, seguros para URL).
Convenciones de nomenclatura
- Nombres de tabla: minúsculas, guiones bajos, en plural (
users,peer_agents) - Nombres de campo: minúsculas con guiones bajos
- Clave primaria:
id(ULID) - Claves foráneas:
{table}_id - Marcas de tiempo:
created_at,updated_at,deleted_at(borrado lógico) - Campos JSON:
*_json
Entidades principales
users
Los usuarios registrados en esta instancia.
CREATE TABLE users (
id CHAR(26) PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE,
phone VARCHAR(32) UNIQUE,
display_name VARCHAR(128),
avatar_url TEXT,
did VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT,
preferences_json JSONB DEFAULT '{}',
llm_keys_json JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_users_did ON users (did);
interface User {
id: string;
username: string;
email?: string;
phone?: string;
display_name?: string;
avatar_url?: string;
did: string;
preferences: UserPreferences;
llm_keys: LLMKeys;
created_at: Date;
updated_at: Date;
deleted_at?: Date;
}
interface UserPreferences {
language: 'zh' | 'en' | 'ja' | 'de' | string;
timezone: string;
notification: { push: boolean; email: boolean };
privacy: { allow_offline_response: boolean };
}
// Las claves son los identificadores de proveedor de `shared/src/llm/catalog.ts`
// (hoy 18) más los proveedores de herramientas (`tavily`). Aquí no se enumeran a
// propósito: se hizo una vez y la lista se quedó congelada en openai / anthropic /
// deepseek / qwen y nunca volvió a ponerse al día. Un proveedor aparece una sola
// vez, en el catálogo.
//
// Los valores son texto cifrado con AES-256-GCM (`ENCRYPTION_KEY`); solo se
// descifran dentro de la pasarela y jamás se envían al cliente.
type LLMKeys = Record<string, EncryptedKey>;
agents
La configuración del Agente de cada usuario. Por ahora un usuario tiene un único Agente principal (v1); más adelante podrán ser varios.
CREATE TABLE agents (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26) NOT NULL REFERENCES users(id),
did VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(128),
description TEXT,
avatar_url TEXT,
primary_language VARCHAR(8) NOT NULL DEFAULT 'zh',
style VARCHAR(32) DEFAULT 'friendly',
model_config_json JSONB DEFAULT '{}',
policies_json JSONB DEFAULT '{}',
capabilities_json JSONB DEFAULT '[]',
is_public BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_agents_user ON agents (user_id);
CREATE INDEX idx_agents_did ON agents (did);
interface Agent {
id: string;
user_id: string;
did: string;
name?: string;
description?: string;
avatar_url?: string;
primary_language: string;
style: 'formal' | 'friendly' | 'technical' | 'casual';
model_config: ModelConfig;
policies: PolicyConfig;
capabilities: Capability[];
is_public: boolean;
}
interface ModelConfig {
brain: ModelChoice;
quick: ModelChoice;
translation: ModelChoice;
summarize: ModelChoice;
}
interface ModelChoice {
provider: string; // identificador de proveedor de catalog.ts; tampoco se enumeran aquí
model: string; // lo devuelve el propio /models del proveedor; no se mantiene localmente
temperature?: number;
}
interface PolicyConfig {
default: 'auto' | 'ask' | 'deny';
rules: PolicyRule[];
}
interface PolicyRule {
peer?: string;
action: 'read' | 'ask' | 'share' | 'commit';
pattern?: string;
effect: 'allow' | 'deny' | 'ask';
}
interface Capability {
type: 'qa' | 'code-generation' | 'translation' | string;
scope: string[];
languages: string[];
}
peer_agents
Los Agentes ajenos que ya conocemos (los contactos). Pueden pertenecer a otro usuario de esta instancia o a otra instancia.
CREATE TABLE peer_agents (
id CHAR(26) PRIMARY KEY,
did VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(128),
description TEXT,
avatar_url TEXT,
organization VARCHAR(255),
endpoint TEXT NOT NULL,
public_key_json JSONB NOT NULL,
agent_facts_json JSONB NOT NULL,
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
etag VARCHAR(255),
trust_level VARCHAR(16) DEFAULT 'unknown',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_peers_did ON peer_agents (did);
peer_contacts
La relación entre un usuario y un Agente ajeno («mis contactos»).
CREATE TABLE peer_contacts (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26) NOT NULL REFERENCES users(id),
peer_id CHAR(26) NOT NULL REFERENCES peer_agents(id),
alias VARCHAR(128),
tags TEXT[],
pinned BOOLEAN DEFAULT false,
muted BOOLEAN DEFAULT false,
policy_overrides_json JSONB DEFAULT '{}',
added_via VARCHAR(32),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, peer_id)
);
CREATE INDEX idx_contacts_user ON peer_contacts (user_id);
conversations
Conversaciones. Pueden ser individuales (usuario↔Agente, usuario↔usuario, Agente↔Agente) o de grupo.
CREATE TABLE conversations (
id CHAR(26) PRIMARY KEY,
type VARCHAR(16) NOT NULL,
name VARCHAR(255),
created_by CHAR(26) NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
archived_at TIMESTAMPTZ
);
CREATE INDEX idx_conversations_created_by ON conversations (created_by);
type: direct_user_agent |
direct_user_user |
direct_agent_agent |
group |
conversation_participants
Los participantes. Tanto los usuarios como los Agentes aparecen como participantes.
CREATE TABLE conversation_participants (
id CHAR(26) PRIMARY KEY,
conversation_id CHAR(26) NOT NULL REFERENCES conversations(id),
participant_type VARCHAR(16) NOT NULL,
user_id CHAR(26) REFERENCES users(id),
agent_id CHAR(26) REFERENCES agents(id),
peer_id CHAR(26) REFERENCES peer_agents(id),
role VARCHAR(16) DEFAULT 'member',
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_read_at TIMESTAMPTZ,
notification VARCHAR(16) DEFAULT 'all'
);
CREATE INDEX idx_participants_conv ON conversation_participants (conversation_id);
CREATE INDEX idx_participants_user ON conversation_participants (user_id);
participant_type: user |
own_agent |
peer_agent |
role: member |
admin |
observer |
messages
CREATE TABLE messages (
id CHAR(26) PRIMARY KEY,
conversation_id CHAR(26) NOT NULL REFERENCES conversations(id),
sender_type VARCHAR(16) NOT NULL,
sender_id CHAR(26) NOT NULL,
sender_did VARCHAR(255),
content_type VARCHAR(32) NOT NULL DEFAULT 'text',
content TEXT,
content_json JSONB,
in_reply_to CHAR(26) REFERENCES messages(id),
thread_root CHAR(26) REFERENCES messages(id),
citations_json JSONB,
language VARCHAR(8),
translation_json JSONB,
via VARCHAR(32),
delivered_at TIMESTAMPTZ,
read_by_json JSONB DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX idx_messages_conversation_created ON messages (conversation_id, created_at DESC);
CREATE INDEX idx_messages_thread_root ON messages (thread_root) WHERE thread_root IS NOT NULL;
sender_type: user |
own_agent |
peer_agent |
system |
||||
content_type: text |
code |
permission_request |
tool_call |
tool_result |
file |
citation |
system_notice |
interface Message {
id: string;
conversation_id: string;
sender_type: 'user' | 'own_agent' | 'peer_agent' | 'system';
sender_id: string;
sender_did?: string;
content_type: ContentType;
content?: string;
content_json?: any;
in_reply_to?: string;
thread_root?: string;
citations?: Citation[];
language?: string;
translation?: { from: string; to: string; provider: string };
via?: 'claude-code' | 'web' | 'mobile' | 'api';
created_at: Date;
}
interface Citation {
source: string;
url?: string;
page?: number;
passage?: string;
trust_level: 'authoritative' | 'verified' | 'unverified';
}
permissions
Registro de auditoría de las solicitudes y decisiones de permisos L2 / L3.
CREATE TABLE permissions (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26) NOT NULL REFERENCES users(id),
peer_id CHAR(26) REFERENCES peer_agents(id),
action VARCHAR(64) NOT NULL,
scope_json JSONB NOT NULL,
level VARCHAR(8) NOT NULL,
decision VARCHAR(16),
decision_scope VARCHAR(16),
requested_by CHAR(26),
decided_by CHAR(26),
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
decided_at TIMESTAMPTZ
);
CREATE INDEX idx_permissions_user_peer ON permissions (user_id, peer_id);
level: L1 |
L2 |
L3 |
|
decision: allow_once |
allow_always |
deny |
pending |
decision_scope: peer |
peer_action |
global |
project_memory
El espejo en el servidor de .claude/peers/. Véase docs/07-project-memory.md.
CREATE TABLE project_memory (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26) NOT NULL REFERENCES users(id),
project_id VARCHAR(255) NOT NULL,
peer_id CHAR(26) NOT NULL REFERENCES peer_agents(id),
facts_md TEXT,
decisions_md TEXT,
meta_json JSONB,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, project_id, peer_id)
);
CREATE INDEX idx_project_memory_user_project ON project_memory (user_id, project_id);
threads
Archivo de los temas largos. Cuando un grupo de mensajes constituye «una decisión de diseño que se ha citado», se marca como hilo y se conserva.
CREATE TABLE threads (
id CHAR(26) PRIMARY KEY,
conversation_id CHAR(26) NOT NULL REFERENCES conversations(id),
root_message_id CHAR(26) NOT NULL REFERENCES messages(id),
title VARCHAR(255),
summary TEXT,
tags TEXT[],
participants_json JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
closed_at TIMESTAMPTZ
);
CREATE INDEX idx_threads_conversation ON threads (conversation_id);
Tablas auxiliares
sessions
Sesiones de inicio de sesión de los usuarios.
CREATE TABLE sessions (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26) NOT NULL REFERENCES users(id),
device_id VARCHAR(64) NOT NULL,
platform VARCHAR(16),
refresh_token_hash TEXT,
last_active_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_sessions_user ON sessions (user_id);
attachments
CREATE TABLE attachments (
id CHAR(26) PRIMARY KEY,
message_id CHAR(26) REFERENCES messages(id),
user_id CHAR(26) NOT NULL REFERENCES users(id),
filename VARCHAR(255) NOT NULL,
content_type VARCHAR(128),
size_bytes BIGINT,
storage_url TEXT NOT NULL,
sha256 CHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
audit_log
Auditoría del tráfico A2A y de las operaciones importantes.
CREATE TABLE audit_log (
id CHAR(26) PRIMARY KEY,
user_id CHAR(26),
action VARCHAR(64) NOT NULL,
details_json JSONB,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_user_created ON audit_log (user_id, created_at DESC);
Convenciones de claves de Redis
Las dos secciones siguientes son el diseño para escalar horizontalmente. Redis y NATS hoy no están ni desplegados ni conectados (se retiraron de
docker-compose*.ymly deenv.tsel 2026-08-07). Las tablas de arriba existen; estas dos secciones no — véase la nota al principio dedocs/02-architecture.md.
session:{token_jti} # datos de sesión, TTL = exp del token
presence:{user_id} # estado en línea; el SET contiene los device_id activos
ratelimit:user:{user_id}:{route} # ventana deslizante
ratelimit:peer:{peer_domain} # límite de tasa por peer
did_cache:{did} # documento DID, TTL 60 s + ETag
agent_facts:{did} # caché de AgentFacts
ws_conn:{user_id} # identificadores de conexión WS activas del usuario
typing:{conversation_id} # quién está escribiendo ahora
unread:{user_id}:{conversation_id} # recuento de no leídos
NATS subjects
user.{user_id}.events # todos los eventos del usuario (la pasarela se suscribe para hacer fan-out)
agent.{agent_id}.tasks # cola de tareas del Agent runtime
conversation.{conv_id}.messages # difusión de mensajes dentro de una conversación
a2a.outbound # cola de peticiones A2A salientes
a2a.inbound # cola de peticiones A2A entrantes