nessie

Instalar Nessie

Ejecuta Nessie en la infraestructura que elijas, desde un único equipo hasta la nube gestionada.

Lo que necesitas antes de empezar

Nessie es un monorepo de pnpm con servicios de Node y dos paquetes estáticos. Sea cual sea la forma en que lo ejecutes, antes tienen que existir estas cuatro cosas.

  • PostgreSQL con pgvector. No es opcional ni sustituible: las migraciones ejecutan CREATE EXTENSION vector y crean columnas vector(1024). En producción se usa pgvector/pgvector:pg17.
  • Node 22 y pnpm. Todos los Dockerfile de infrastructure/docker/ se basan en node:22-slim, y el repositorio fija su gestor de paquetes en el package.json raíz (pnpm@10.22.0).
  • Un endpoint de modelo compatible con OpenAI y una clave para él. Los agentes y los embeddings pasan por él. Consulta «Configurar la inferencia» más abajo.
  • Almacenamiento de objetos compatible con S3, para cualquier despliegue que no sea el de desarrollo local. Las subidas en disco local se rechazan fuera del modo local, y ese rechazo es un fallo al arrancar y no un arranque degradado; consulta «Una máquina o una nube gestionada».

Docker y un proxy inverso hacen falta para la vía con contenedores que se describe más adelante, y un dominio con TLS para cualquier cosa accesible desde internet.

El dimensionamiento del hardware (CPU, memoria y disco mínimos para un número dado de personas) no aparece en ningún lugar del repositorio, así que esta página tampoco lo indica.

Los componentes y cómo encajan

ComponenteQué esEntrada
api/La API HTTP/WebSocket con Fastify. Todos los clientes hablan con ella y con nada más.Sí
worker/Orquestación de agentes y ejecuciones en segundo plano. Consulta la cola de trabajos y no abre ningún puerto.No
admin/La interfaz del producto: un paquete estático generado con Vite y servido por nginx.Sí
web/El sitio público. Estático, opcional y sin relación con el funcionamiento del producto.Sí
PostgreSQLLa base de datos, la cola de trabajos y el bus de tiempo real, las tres cosas.No
Almacenamiento de objetosAdjuntos, avatares y archivos de la base de conocimiento. Compatible con S3; en producción se usa MinIO.No
gateway/Un relé de notificaciones push opcional para APNs y FCM. Está desactivado salvo que lo actives de forma deliberada.Sí

La API y el worker son una sola imagen. Los dos paquetes están muy interrelacionados, así que Dockerfile.app compila todo el equipo una vez y el contenedor del worker sustituye el comando por node worker/dist/index.js. En modo local no hay ningún contenedor de worker: la API ejecuta el worker integrado en su propio proceso.

No hay Redis. La cola de trabajos y el transporte en tiempo real se basan en Postgres, por decisión y no por descuido; el árbol de Google Cloud elimina sus módulos de Pub/Sub y Redis por el mismo motivo.

Puertos

En desarrollo local, la API está en 5454 y el admin en 5455 de forma predeterminada. Una copia de trabajo puede tomar su propio par configurando NESSIE_API_PORT y NESSIE_ADMIN_PORT, en el entorno o en el .env del repositorio; todas las herramientas del repositorio, incluidas las suites de pruebas de navegador, leen esos mismos dos valores. Elige el par antes de arrancar y mantenlo: mover un servicio en marcha a otro puerto hace que el admin hable con la API equivocada.

En producción la cosa es distinta, y esa regla no cambia. El contenedor de la API escucha internamente en el 5554 (fijado con NESSIE_API_PORT) y el relé push en el 5556, ambos detrás del proxy inverso.

Ejecutarlo en una sola máquina, para desarrollo

El repositorio incluye un archivo de Compose que solo contiene una base de datos, que es todo lo que necesita el ciclo de desarrollo local.

pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgres

Ese contenedor publica Postgres en el 55432, con el usuario, la contraseña y la base de datos llamados todos nessie. Pon la cadena de conexión en un .env en la raíz del repositorio (el script de desarrollo de la API lo lee) y luego aplica las migraciones y compila el worker una vez, porque en modo local la API carga el worker desde su dist compilado:

DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie

pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm dev

pnpm dev ejecuta la API y el admin en paralelo, con recarga en caliente, en el 5454 y el 5455. Comprueba que la API responde con GET /api/health.

Postgres y el requisito de pgvector

Las migraciones crean tres extensiones: vector, pg_trgm y pgcrypto. Por eso el rol que las ejecuta debe tener permiso para crear extensiones. En un Postgres gestionado, es lo primero que hay que comprobar.

El ancho de los vectores forma parte del esquema, no es un ajuste. thoughts.embedding, thought_recalls.query_embedding y knowledge_page_chunks.embedding son columnas vector(N), y N se indica una sola vez como EMBEDDING_DIMENSIONS en packages/schemas/src/embedding.ts; actualmente vale 1024, el ancho nativo del modelo de embeddings que usa producción. Cada petición de embedding envía ese ancho, así que un proveedor que respondiera con otro falla de forma ruidosa en lugar de escribir vectores que la base de datos rechazaría. Cambiar a un modelo de embeddings de otro ancho exige editar esa constante, escribir una migración que vuelva a tipar las tres columnas y volver a generar los embeddings: los vectores de distinto ancho no son convertibles, así que la migración los anula en lugar de truncarlos.

Si ejecutas Nessie junto a otras aplicaciones en una red Docker compartida, dale al contenedor de su base de datos un nombre distinto. El servicio de Compose del proyecto se llama nessie-postgres y nunca postgres, porque el nombre del servicio se convierte en un alias DNS en la red compartida, y una colisión haría que postgres:5432 alternara entre dos bases de datos por turnos (round-robin).

Configuración

La configuración en tiempo de ejecución es por capas: un archivo nessie.config.json montado en modo solo lectura en la API y en el worker, con las variables de entorno por encima. La correspondencia entre ambos es ConfigEnvMap en packages/config, que es la lista de referencia.

NESSIE_MODE elige la forma del despliegue: local (el valor por defecto y la vía del desarrollador), hosted o selfHosted, que es lo que establece una instalación de alojamiento propio. Desactiva el inicio de sesión de desarrollo y exige una lista de CORS permitidos.

Estos son los ajustes sin los que un despliegue selfHosted no arrancará o no funcionará:

VariablePara qué sirve
DATABASE_URL / NESSIE_DB_URLLa cadena de conexión a Postgres.
NESSIE_AUTH_SECRET32 bytes en hexadecimal (openssl rand -hex 32). Firma las sesiones y los tokens de arranque. El arranque falla sin él fuera del modo local, y tiene que ser el mismo valor en cada réplica y entre despliegues, o todas las sesiones caducan en cada versión.
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSIONUn anillo de claves en JSON y la etiqueta que selecciona la raíz activa; es obligatorio fuera del modo local. Es una raíz distinta de la clave de firma: el anillo cifra cada secreto duradero que guarda Nessie. No reutilices una como la otra.
NESSIE_STORAGE_PROVIDERDebe ser s3. filesystem es el valor por defecto y se rechaza en los modos hosted y selfHosted, de modo que un despliegue que no configure nada aquí no arranca, en lugar de escribir las subidas en un disco al que ningún otro contenedor puede acceder.
NESSIE_CORS_ORIGINSEl origen del admin. El admin y la API son orígenes distintos, y la API debe incluir en su lista de permitidos el que sirve.
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URLSe usa para generar las URI de redirección de OAuth fuera de una petición HTTP. La API lanza un error en el momento de la petición si falta la primera.
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEYA dónde va la inferencia. Consulta más abajo.

infrastructure/compose/.env.prod.example muestra la forma exacta de todos ellos, con los bloques opcionales (almacenamiento de objetos, almacén de secretos, SSO y correo conectado) comentados en su sitio. El catálogo completo, con cada ajuste opcional, está en docs/deployment/configuration.md.

Desplegar con Docker Compose

Copia .env.prod.example como .env junto a docker-compose.prod.yml y rellénalo; después levanta la base de datos, compila, migra y arranca. El orden importa: las migraciones y los seeds se ejecutan contra una base de datos que ya está activa, antes de que ningún servicio atienda peticiones.

COMPOSE="docker compose -f infrastructure/compose/docker-compose.prod.yml"

bash infrastructure/compose/ensure-encryption-key-ring.sh infrastructure/compose/.env
$COMPOSE up -d nessie-postgres
$COMPOSE build nessie-api nessie-admin nessie-web
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api prisma:migrate:deploy
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api seed:connectors
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api seed:apps
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api reconcile
$COMPOSE up -d

Los Dockerfile de producción ejecutan el lint de cada paquete antes de compilarlo, así que un fallo de lint es un fallo de compilación.

El paso de reconcile no es opcional

reconcile inserta las reglas de política por defecto de cada organización y completa las concesiones de herramientas del MCP protegido y del Personal Assistant. Antes la API lo hacía al arrancar en cada réplica; ahora solo se conecta, escucha y no hace nada más, así que sin este paso la primera organización no tiene reglas de política y cada vinculación de un agente se deniega. Es idempotente, así que ejecutarlo en cada despliegue no tiene coste, y una actualización aplicada a mano también tiene que ejecutarlo.

La primera cuenta de propietario

Una instalación selfHosted nueva no tiene usuarios, así que la API genera un token de arranque de un solo uso y registra una URL de configuración. El token vive en Postgres y no en un proceso, de modo que todas las réplicas coinciden en él.

docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrap

Abre https://<your-admin-host>/bootstrap?token=<token> y crea la cuenta de propietario. El token caduca a los 15 minutos; reinicia el servicio de la API para generar uno nuevo.

Si configuras SSO, no hay ningún paso de arranque. El modo de arranque se suprime en cuanto se activa un proveedor de autenticación externo, porque de lo contrario secuestraría la pantalla de inicio de sesión; y la primera persona que entra con SSO crea el equipo por defecto y pasa a ser propietaria de ese equipo.

Configurar la inferencia

El chat es compatible con OpenAI. NESSIE_MODEL_BASE_URL es el endpoint, NESSIE_MODEL_API_KEY el token de portador y NESSIE_MODEL_PROVIDER indica el adaptador: Nessie tiene adaptadores compilados para openai, kimi y deepseek, y cualquier otro proveedor se conecta a través del conector genérico compatible con OpenAI. NESSIE_MODEL_NAME define el modelo por defecto del despliegue, aunque la selección de modelo de cada agente tiene prioridad sobre él.

Los embeddings se enrutan por separado, porque el proveedor del chat puede no ofrecerlos en absoluto: DeepSeek, por ejemplo, no tiene endpoint de embeddings. Cada campo NESSIE_EMBEDDING_* que no se defina hereda del proveedor del chat, así que un despliegue que no define ninguno genera los embeddings a través del endpoint del chat. Define NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL y, en un proxy que enruta por servicio, el segmento de servicio, cuando el proveedor del chat no pueda generar embeddings. Equivocarse aquí falla en silencio en lugar de hacerlo de forma ruidosa: la recuperación de memoria y la búsqueda en la base de conocimiento se degradan a una búsqueda léxica y siguen funcionando, dejando constancia en el registro con kb_search: query embedding failed.

La contabilidad de gasto propia de Nessie (el registro de tokens por organización y el control de presupuesto) no se ve afectada por el endpoint al que apuntes, y funciona igual tanto si hay atribución firmada por llamada configurada como si no.

Iniciar sesión

Nessie autentica a las personas a través de UnlikeOtherAI, que también gestiona la estructura de organizaciones y equipos. La página de inicio de sesión del admin muestra un único botón «Login / Register».

La integración es un flujo de JWT de configuración, no un OIDC estándar: la API sirve un documento de configuración firmado en RS256 en GET /api/auth/sso/config y el JWKS correspondiente en GET /.well-known/jwks.json, ambos en el host de la API. Ponerlo en marcha es una secuencia que se hace una sola vez: genera un par de claves RSA-2048 y define UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL y UOA_CONTACT_EMAIL; pulsa el botón una vez, lo que crea una solicitud de integración; espera a que la aprueben, lo que envía un enlace de un solo uso a la dirección de contacto con el secreto de cliente; define UOA_CLIENT_SECRET y reinicia la API. El kid debe ser único por dominio.

Ponerlo detrás de un dominio, con TLS

Un despliegue sirve tres nombres: el sitio público, el admin y la API. El origen del admin y el de la API no son intercambiables. Dockerfile.admin incorpora VITE_API_BASE_URL en el paquete en tiempo de compilación, así que cada artefacto del admin ya compilado, incluido un build de escritorio que lo incrusta, debe compilarse contra el origen de la API. Si se compila contra el origen propio del admin, /api/auth/providers se resuelve en el HTML del admin y el inicio de sesión se queda atascado en «Loading providers...».

En producción, TLS termina en Caddy, que obtiene certificados por nombre de host mediante HTTP-01 y no necesita un plugin del proveedor DNS para ello. Apunta el proxy a los contenedores del admin y del sitio web en el puerto 80, y a la API en su puerto interno.

Define NESSIE_API_TRUSTED_PROXY_HOPS con el número de proxies delante de la API: 1 detrás de un único proxy inverso. Su valor por defecto es 0, que ignora X-Forwarded-For por completo. Es una sola decisión con dos consecuencias: también es lo que agrupa los límites de frecuencia de autenticación, así que si es demasiado bajo, todos los clientes comparten el cubo del proxy, y si es demasiado alto, un cliente puede falsificar su propia dirección. Quédate por lo bajo y mide.

Servir cada organización y equipo en su propio subdominio es posible y está desactivado por defecto. Se rige por NESSIE_TEAM_HOST_BASE_DOMAIN: si no está definido, los inquilinos no se enrutan por nombre de host en absoluto; y la puerta de emisión de certificados NESSIE_TLS_CHECK_KEY rechaza todo mientras no esté definida, que es el valor por defecto correcto para un despliegue que no usa esta función.

Comprobaciones de estado

EndpointPregunta que responde
GET /api/health¿Sigue vivo este proceso? Solo falla cuando empieza a vaciarse.
GET /api/health/ready¿Puede esta réplica aceptar nuevas peticiones? Es el que conviene apuntar desde un balanceador de carga.
GET /api/ops/health¿Cómo va el despliegue? Un informe para una persona (latidos de los workers, profundidad de la cola, trabajos muertos), no una sonda.

Con SIGTERM, la API termina lo que tiene en curso en lugar de caerse donde está. NESSIE_SHUTDOWN_TIMEOUT_MS acota ese cierre ordenado y por defecto vale 25 segundos; mantenlo por debajo del periodo de gracia que tu orquestador conceda al contenedor, o el runtime matará el proceso antes y el cierre ordenado no servirá de nada.

Actualizar

Una actualización consiste en prisma migrate deploy contra la base de datos existente, después los seeds y luego reconcile. Los mismos cuatro comandos que el primer despliegue.

La ruta se prueba con la que sigue de verdad quien aloja Nessie por su cuenta, en lugar de con una base de datos nueva: el CI restaura una instantánea tomada veinte migraciones atrás, la migra hasta el head actual, valida el esquema y ejecuta una comprobación de humo sobre las tablas principales. Cualquier versión dentro de esa ventana final es un origen de actualización probado. Las carpetas de migración son inmutables una vez confirmadas, y un paso de lint hace fallar la compilación si se renombra, renumera, elimina o modifica alguna, porque las cuatro cosas rompen migrate deploy para una base de datos que ya registró la fila antigua.

Una migración fallida bloquea todos los despliegues posteriores. migrate deploy se detiene en el primer fallo y rechaza todas las migraciones posteriores con P3009, así que la instalación se queda en la versión anterior hasta que alguien lo desbloquee a mano. La producción del propio proyecto pasó un día rechazando todos los despliegues de esta forma. Desbloquearlo exige confirmar que la migración fallida se revirtió de verdad: ninguno de los objetos que crea debe existir, y lo que altera debe conservar su forma original. Solo entonces se marca como revertida. Una migración aplicada a medias no es candidata.

La copia de seguridad y la restauración del despliegue de alojamiento propio con Docker Compose no están documentadas en el repositorio, así que esta página no describe ningún procedimiento para ello.

Una máquina o una nube gestionada

La vía de Docker Compose en un único host es la que está probada: es lo que ejecuta la producción del propio proyecto, en un solo servidor, con la base de datos, el almacenamiento de objetos, la API, el worker, el admin y el sitio como contenedores detrás de un proxy inverso compartido.

Escalar más allá de una máquina cambia tres cosas, todas ellas aplicadas por el sistema y no solo recomendadas. Fuera del modo local, Nessie ejecuta la API y el worker como procesos separados y como varias copias de cada uno, así que cualquier capacidad que suponga que un solo proceso es dueño del disco de la máquina se rechaza: el almacenamiento de objetos en el sistema de archivos (un fallo al arrancar), las plantillas de entorno de ejecución con el proveedor docker (inertes, y el aprovisionamiento falla de forma ruidosa) y las herramientas integradas file_read, file_write y file_glob (un resultado de herramienta fallido que el agente puede leer). Cada rechazo indica el ajuste, el modo y qué usar en su lugar.

Para una nube gestionada, el repositorio incluye un árbol de Terraform en infrastructure/terraform/ orientado a Google Cloud: la API como servicio de Cloud Run con un mínimo de una instancia (mantiene un cliente LISTEN persistente de Postgres y ejecuta barridos de mantenimiento, así que escalarla a cero detiene la entrega en tiempo real), el worker como pool de workers de Cloud Run, porque no abre ningún puerto y un Service nunca llegaría a estar listo, un job de migración que condiciona cada despliegue, Cloud SQL para PostgreSQL 17 y un bucket de GCS al que se accede a través del backend compatible con S3.

Esa ruta está escrita, pero no recorrida. El repositorio lo dice sin rodeos, y esta página también: no existe ningún proyecto de Google Cloud para ella, nada del árbol se ha planificado ni aplicado contra un proyecto real, la ruta de subida multiparte a GCS no se ha probado con un bucket real, el tipo de recurso de pool de workers no está validado, y el número de saltos de proxy de confianza es un marcador de posición que hay que medir, no una medición. El admin y el sitio público no están modelados ahí en absoluto. Trátalo como un punto de partida que tienes que verificar, no como un despliegue soportado.

Dónde viven los datos de un despliegue, qué guarda el software y qué no guarda a propósito, es una página aparte: Hecho en la UE y dónde están tus datos.