Traduzido do original em chinês. Havendo divergência, o original prevalece.
Confer — Modelo de dados
Esquema das tabelas do PostgreSQL e definições de tipos em TypeScript. Todos os identificadores são ULID (ordenados por tempo, seguros em URL).
Convenções de nomenclatura
- Nomes de tabela: minúsculas, underscores, no plural (
users,peer_agents) - Nomes de campo: minúsculas com underscores
- Chave primária:
id(ULID) - Chaves estrangeiras:
{table}_id - Carimbos de tempo:
created_at,updated_at,deleted_at(exclusão lógica) - Campos JSON:
*_json
Entidades principais
users
Os usuários registrados nesta instância.
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 };
}
// As chaves são os identificadores de fornecedor de `shared/src/llm/catalog.ts`
// (hoje 18), mais os fornecedores de ferramentas (`tavily`). Aqui não são
// enumerados de propósito: já foram uma vez, e a lista parou em openai /
// anthropic / deepseek / qwen sem nunca mais acompanhar. Um fornecedor aparece
// uma única vez, no catálogo.
//
// Os valores são texto cifrado em AES-256-GCM (`ENCRYPTION_KEY`); só são
// decifrados dentro do gateway e nunca chegam ao cliente.
type LLMKeys = Record<string, EncryptedKey>;
agents
A configuração do Agente de cada usuário. Por enquanto um usuário tem um único Agente principal (v1); vários serão possíveis depois.
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 fornecedor do catalog.ts, também não enumerado aqui
model: string; // devolvido pelo /models do próprio fornecedor, nunca mantido 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
Os Agentes alheios que já conhecemos (os contatos). Podem ser de outro usuário desta instância ou de outra instância.
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
A relação entre um usuário e um Agente alheio («meus contatos»).
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
As conversas. Podem ser individuais (usuário↔Agente, usuário↔usuário, Agente↔Agente) ou em 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
Os participantes. Tanto usuários quanto Agentes aparecem 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 auditoria das solicitações e decisões de permissão 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
O espelho no servidor de .claude/peers/. Veja 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
O arquivamento dos tópicos longos. Quando um conjunto de mensagens constitui «uma decisão de projeto que já foi citada», é marcado como tópico e preservado.
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);
Tabelas auxiliares
sessions
As sessões de login dos usuários.
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
Auditoria do tráfego A2A e das operações 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);
Convenções de chaves do Redis
As duas seções seguintes são o desenho para escalar horizontalmente. Redis e NATS hoje não estão implantados nem conectados (removidos de
docker-compose*.ymle deenv.tsem 2026-08-07). As tabelas acima existem; estas duas seções não — veja a nota no início dedocs/02-architecture.md.
session:{token_jti} # dados da sessão, TTL = exp do token
presence:{user_id} # status on-line; o SET contém os device_id ativos
ratelimit:user:{user_id}:{route} # janela deslizante
ratelimit:peer:{peer_domain} # limite de taxa por peer
did_cache:{did} # documento DID, TTL 60 s + ETag
agent_facts:{did} # cache de AgentFacts
ws_conn:{user_id} # identificadores das conexões WS ativas do usuário
typing:{conversation_id} # quem está digitando agora
unread:{user_id}:{conversation_id} # contagem de não lidas
NATS subjects
user.{user_id}.events # todos os eventos do usuário (o gateway assina para fazer fan-out)
agent.{agent_id}.tasks # fila de tarefas do Agent runtime
conversation.{conv_id}.messages # difusão de mensagens dentro de uma conversa
a2a.outbound # fila de requisições A2A de saída
a2a.inbound # fila de requisições A2A de entrada