Traducción. Autoritativo: fs-core-0011-custom-attributes.md.
Contexto
Todo tenant tiene campos que no anticipamos, y las dos respuestas obvias son ambas incorrectas. Un JSONB libre es inconsultable e intipable, así que el constructor de segmentos no lo puede ofrecer y la validación no lo puede proteger. Una migración por tenant no es un producto. La respuesta es una definición tipada: el tenant declara el atributo y su tipo una vez, y esa declaración alimenta la validación Zod al escribir, los campos disponibles en el constructor de segmentos y la presentación en la consola. Los valores viven en JSONB sobre el contacto; el schema de ese JSONB es dato.Alcance (normativo)
core.attribute_definitions: clave, etiqueta, tipo y restricciones por tenant.- Tipos:
string,number,date,boolean,enum. - Generación de esquema Zod desde las definiciones, cacheada por tenant.
- Validación de los atributos personalizados del contacto en cada escritura.
- Exposición al DSL de segmentos como objetivos de condición tipados.
- Promoción de un atributo caliente a columna real, como operación documentada.
Fuera de alcance (normativo)
- Atributos sobre entidades distintas del contacto. Loyalty y CRM definen los suyos si los necesitan.
- Atributos calculados o derivados. Un valor que se calcula es un segmento, no un atributo.
- Definiciones compartidas entre tenants. Toda definición pertenece a exactamente un tenant.
Comportamiento (normativo)
- Una definición está acotada a un tenant.
keyes único por tenant e inmutable — renombrarlo dejaría huérfano a todo segmento que lo referencie. - El tipo es inmutable tras la creación. Cambiar
stringanumbercontra valores existentes es una migración, no una edición; la operación correcta es una definición nueva. - Los valores se validan contra el esquema Zod generado en cada escritura. Un valor inválido se rechaza — un atributo tipado que acepta cualquier cosa en silencio es un JSONB disfrazado.
- Las definiciones
enumllevan sus valores permitidos; agregar uno está bien, quitar uno solo se permite cuando ningún contacto lo tiene. - Las definiciones se cachean por tenant y se invalidan al cambiar. La validación está en el camino de escritura y nunca debe unir tablas.
- Borrar una definición es un borrado lógico. Los valores quedan sobre los contactos; solo se retira la definición, porque segmentos y exportaciones pueden seguir referenciando datos históricos.
- El DSL de segmentos ve las definiciones como objetivos de condición tipados — un atributo
dateofrece operadores de fecha, unonumberofrece numéricos. - Promoción: un atributo usado en caminos calientes se puede promover a columna real mediante una migración documentada. La definición pasa a apuntar a la columna y se descarta la copia JSONB. Esta es la salida de emergencia que mantiene honesto al patrón a escala.
- PROHIBIDO: un atributo personalizado que contenga un documento de identidad o cualquier valor que la plataforma ya modela.
Datos (normativo)
Los valores viven en
core.contacts.custom_attributes JSONB, con índice GIN para las consultas de
segmentos.
API (normativo)
Eventos (normativo)
Ninguno. Un cambio de definición es administrativo y queda cubierto por la auditoría; los cambios de atributos del contacto ya emitencore.contact.updated.
Criterios de aceptación (normativo)
- Un valor que viola su tipo se rechaza con un error tipado que nombra el atributo.
keyytypeno se pueden cambiar tras la creación.- Quitar un valor de
enumque todavía tiene un contacto se rechaza. - Una definición con borrado lógico deja sus valores intactos y legibles.
- Las condiciones de segmento sobre un atributo
dateofrecen operadores de fecha y rechazan una comparación de string. - La validación no hace ninguna unión de base de datos en el camino de escritura.
- Un atributo promovido se lee desde su columna sin que cambie nada desde la vista del llamador.
- Negativo: una definición con una clave que colisiona con un campo de plataforma se rechaza.
Ejecución
Un solo slice, comando síncrono. F1b. El generador Zod vive enpackages/core junto al generador de
unions de catálogos — mismo patrón, misma disciplina de caché.