Instalar o Nessie
O Nessie funciona na infraestrutura escolhida, desde uma única máquina até à cloud gerida.
O que é preciso antes de começar
O Nessie é um monorepo pnpm com serviços Node e dois conjuntos estáticos. Seja qual for a forma de execução, é preciso ter primeiro estas quatro coisas.
- PostgreSQL com
pgvector. Não é opcional nem substituível: as migrações executamCREATE EXTENSION vectore criam colunasvector(1024). A produção usapgvector/pgvector:pg17. - Node 22 e pnpm. Todos os Dockerfiles em
infrastructure/docker/são compilados sobrenode:22-slim, e o repositório fixa o gestor de pacotes nopackage.jsonda raiz (pnpm@10.22.0). - Um endpoint de modelo compatível com OpenAI e uma chave para o usar. Os agentes e os embeddings passam ambos por ele. Ver «Configurar a inferência» abaixo.
- Armazenamento de objetos compatível com S3, para qualquer implementação que não seja o ambiente local de desenvolvimento. Os envios em disco local são recusados fora do modo
local, e essa recusa é uma falha no arranque e não um arranque degradado — ver «Uma só máquina ou uma cloud gerida».
O Docker e um proxy inverso são necessários para o caminho com contentores descrito mais adiante, e um domínio com TLS para tudo o que seja acessível a partir da Internet.
O dimensionamento de hardware — o mínimo de CPU, memória e disco para um determinado número de pessoas — não está indicado em lado nenhum do repositório, por isso esta página também não o indica.
Os componentes e a forma como se articulam
| Componente | O que é | Entrada |
|---|---|---|
api/ | A API HTTP/WebSocket baseada em Fastify. Todos os clientes comunicam com esta API e com mais nenhuma. | Sim |
worker/ | Orquestração de agentes e execuções em segundo plano. Consulta a fila de tarefas e não abre nenhuma porta. | Não |
admin/ | A interface do produto — um conjunto estático criado pelo Vite e servido pelo nginx. | Sim |
web/ | O site público. Estático, opcional e sem relação com a execução do produto. | Sim |
| PostgreSQL | A base de dados, a fila de tarefas e o canal de tempo real — os três em simultâneo. | Não |
| Armazenamento de objetos | Anexos, avatares e ficheiros da base de conhecimento. Compatível com S3; MinIO em produção. | Não |
gateway/ | Um retransmissor opcional de notificações push para APNs e FCM. Fica desligado, salvo se for ativado deliberadamente. | Sim |
A API e o worker são uma só imagem. Os dois pacotes estão muito interligados, por isso o Dockerfile.app compila todo o conjunto uma única vez, e o contentor do worker substitui o comando por node worker/dist/index.js. No modo local não existe contentor de worker: a API executa o worker incorporado no próprio processo.
Não há Redis. A fila de tarefas e o transporte em tempo real assentam ambos em Postgres, por decisão e não por omissão — a árvore do Google Cloud elimina os seus módulos Pub/Sub e Redis pela mesma razão.
Portas
No desenvolvimento local, a API está por predefinição na porta 5454 e a admin na 5455. Uma cópia de trabalho do repositório pode ter o seu próprio par, definindo NESSIE_API_PORT e NESSIE_ADMIN_PORT, no ambiente ou no .env do repositório; todas as ferramentas do repositório, incluindo as suítes de testes no navegador, leem os mesmos dois valores. Escolha o par antes de arrancar e mantenha-o: mudar um serviço em execução para outra porta faz com que a admin fale com a API errada.
A produção é diferente e não é afetada por essa regra. O contentor da API escuta internamente na porta 5554 (fixada com NESSIE_API_PORT) e o retransmissor push na 5556, ambos por trás do proxy inverso.
Executar tudo numa só máquina, para desenvolvimento
O repositório traz um ficheiro Compose apenas com uma base de dados, que é tudo o que o ciclo de desenvolvimento local precisa.
pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgresEsse contentor publica o Postgres na porta 55432, com o utilizador, a palavra-passe e a base de dados todos com o nome nessie. Coloca-se a cadeia de ligação num .env na raiz do repositório — o script de desenvolvimento da API lê-o — e depois aplicam-se as migrações e compila-se o worker uma vez, porque no modo local a API carrega o worker a partir do seu dist compilado:
DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie
pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm devO pnpm dev executa a API e a admin em paralelo, com recarregamento a quente, nas portas 5454 e 5455. Confirme-se que a API respondeu em GET /api/health.
Postgres e o requisito pgvector
As próprias migrações criam três extensões — vector, pg_trgm e pgcrypto —, por isso a função que as executa tem de poder criar extensões. Num Postgres gerido, é isso que convém verificar antes de tudo o resto.
A largura dos vetores faz parte do esquema, não é uma definição. As colunas thoughts.embedding, thought_recalls.query_embedding e knowledge_page_chunks.embedding são colunas vector(N), e N está indicado uma única vez como EMBEDDING_DIMENSIONS em packages/schemas/src/embedding.ts — atualmente 1024, a largura nativa do modelo de embeddings usado em produção. Cada pedido de embedding envia essa largura, pelo que um fornecedor que respondesse com outra falha de forma explícita, em vez de gravar vetores que a base de dados rejeita. Mudar para um modelo de embeddings de outra largura exige alterar essa constante, escrever uma migração que altere o tipo das três colunas e voltar a calcular os embeddings: vetores de larguras diferentes não são convertíveis, por isso a migração os anula em vez de os truncar.
Se executar o Nessie junto de outras aplicações numa rede Docker partilhada, dê ao contentor da base de dados um nome distinto. O serviço Compose do projeto chama-se nessie-postgres e nunca postgres, porque o nome do serviço passa a ser um alias DNS na rede partilhada, e uma colisão faz com que postgres:5432 distribua as ligações alternadamente por duas bases de dados.
Configuração
A configuração em tempo de execução é feita por camadas: um ficheiro nessie.config.json montado em modo de leitura na API e no worker, com as variáveis de ambiente por cima. A correspondência entre os dois está em ConfigEnvMap em packages/config, que é a lista de referência.
O NESSIE_MODE escolhe a forma de implementação: local (a predefinição e o caminho do programador), hosted ou selfHosted — que é o que uma instalação própria define. Desativa o início de sessão de desenvolvimento e exige uma lista de origens CORS permitidas.
Estas são as definições sem as quais uma implementação selfHosted não arranca, ou não funciona:
| Variável | Porquê |
|---|---|
DATABASE_URL / NESSIE_DB_URL | A cadeia de ligação ao Postgres. |
NESSIE_AUTH_SECRET | 32 bytes em hexadecimal (openssl rand -hex 32). Assina as sessões e os tokens de arranque. Fora do modo local o arranque falha sem ele, e tem de ser o mesmo valor em todas as réplicas e entre implementações, senão todas as sessões terminam em cada nova versão. |
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSION | Um anel de chaves em JSON e o rótulo que seleciona a raiz ativa, obrigatórios fora do modo local. Esta raiz é separada da chave de assinatura — o anel cifra todos os segredos duradouros que o Nessie guarda. Não se deve reutilizar uma como a outra. |
NESSIE_STORAGE_PROVIDER | Tem de ser s3. filesystem é a predefinição e é recusado nos modos hosted e selfHosted, por isso uma implementação que não defina nada aqui falha no arranque em vez de gravar os envios num disco que nenhum outro contentor consegue ler. |
NESSIE_CORS_ORIGINS | A origem da admin. A admin e a API são origens separadas, e a API tem de incluir na lista de permitidos a origem que serve. |
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URL | Usados para gerar os URIs de redirecionamento do OAuth fora de um pedido HTTP. Sem eles, a API lança um erro no momento do pedido. |
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY | Para onde vai a inferência. Ver abaixo. |
O infrastructure/compose/.env.prod.example mostra a forma exata de todas elas, com os blocos opcionais — armazenamento de objetos, cofre de segredos, SSO, correio ligado — comentados no próprio sítio. A lista completa, incluindo todas as definições opcionais, está em docs/deployment/configuration.md.
Implementar com Docker Compose
Copia-se o .env.prod.example para .env ao lado do docker-compose.prod.yml e preenche-se; depois arranca-se a base de dados, compila-se, aplicam-se as migrações e inicia-se o serviço. A ordem importa: as migrações e os dados iniciais são executados numa base de dados que já está ativa, antes de qualquer serviço responder.
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 -dOs Dockerfiles de produção executam o lint de cada pacote antes de o compilar, por isso uma falha de lint é uma falha de compilação.
O passo de reconciliação não é opcional
reconcile cria as regras de política predefinidas de cada organização e preenche as concessões de ferramentas protegidas por MCP e do Assistente Pessoal. A API fazia isto no arranque, em cada réplica; agora liga-se, fica à escuta e não faz mais nada, por isso sem este passo a primeira organização não tem regras de política e todas as vinculações de agentes são recusadas. É idempotente, pelo que executá-lo em cada implementação não tem custo — e uma atualização aplicada manualmente também tem de o executar.
A primeira conta de proprietário
Uma instalação selfHosted nova não tem utilizadores, por isso a API cria um token de arranque de utilização única e escreve nos registos um URL de configuração. O token fica no Postgres e não num processo, para que todas as réplicas o reconheçam da mesma forma.
docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrapAbra-se o https://<your-admin-host>/bootstrap?token=<token> e crie-se a conta de proprietário. O token expira ao fim de 15 minutos; para obter um novo, reinicie-se o serviço da API.
Se configurar o SSO, não há qualquer passo de arranque. O modo de arranque inicial é suspenso no momento em que um fornecedor de autenticação externo é ativado — caso contrário, tomaria o ecrã de início de sessão — e a primeira pessoa a iniciar sessão através do SSO cria a equipa predefinida e torna-se o seu proprietário.
Configurar a inferência
O chat é compatível com OpenAI. NESSIE_MODEL_BASE_URL é o endpoint, NESSIE_MODEL_API_KEY o token bearer, e NESSIE_MODEL_PROVIDER indica o adaptador — o Nessie inclui adaptadores para openai, kimi e deepseek, e chega a qualquer outro através do conector genérico compatível com OpenAI. NESSIE_MODEL_NAME define o modelo predefinido da implementação, ao qual a seleção de modelo de cada agente continua a ter prioridade.
Os embeddings são encaminhados à parte, porque o fornecedor de chat pode não os oferecer de todo — a DeepSeek, por exemplo, não tem um endpoint de embeddings. Cada campo NESSIE_EMBEDDING_* que não esteja definido herda o fornecedor de chat, pelo que uma implementação que não defina nenhum calcula os embeddings através do endpoint de chat. Definem-se NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL e, num proxy que encaminhe por serviço, o segmento do serviço, quando o fornecedor de chat não conseguir calcular embeddings. Se isto estiver mal configurado, a falha é silenciosa e não visível: a memória e a pesquisa na base de conhecimento passam para um canal lexical e continuam a funcionar, registando kb_search: query embedding failed sempre que isso acontece.
A contabilização de consumo própria do Nessie — o registo de tokens por organização e o controlo de orçamento — não depende do endpoint para o qual aponta, e funciona da mesma forma com ou sem atribuição assinada por chamada configurada.
Iniciar sessão
O Nessie autentica as pessoas através da UnlikeOtherAI, que também gere a estrutura de organizações e equipas. A página de início de sessão da admin mostra um único botão «Iniciar sessão / Registar».
A integração é um fluxo de JWT de configuração e não um OIDC padrão: a API serve um documento de configuração RS256 assinado em GET /api/auth/sso/config e o JWKS correspondente em GET /.well-known/jwks.json, ambos no servidor da API. A montagem é uma sequência única: gera-se um par de chaves RSA-2048 e definem-se UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL e UOA_CONTACT_EMAIL; carrega-se uma vez no botão, o que abre um pedido de integração; aguarda-se a aprovação, que envia para o endereço de contacto uma ligação de utilização única com o segredo do cliente; define-se UOA_CLIENT_SECRET e reinicia-se a API. O kid tem de ser único por domínio.
Colocar atrás de um domínio, com TLS
Uma implementação serve três nomes: o site público, a admin e a API. A origem da admin e a da API não são intercambiáveis. O Dockerfile.admin incorpora o VITE_API_BASE_URL no conjunto durante a compilação, por isso todos os artefactos da admin compilados — incluindo uma compilação para desktop que a incorpore — têm de ser compilados contra a origem da API. Se forem compilados contra a origem da própria admin, o /api/auth/providers resolve para o HTML da admin, e o início de sessão fica preso em «A carregar fornecedores...».
Em produção, o TLS termina no Caddy, que obtém certificados por nome de anfitrião através de HTTP-01 e não precisa de nenhum plugin do fornecedor de DNS para isso. Aponte-se o proxy para os contentores da admin e do site na porta 80 e para a API na sua porta interna.
Defina-se NESSIE_API_TRUSTED_PROXY_HOPS com o número de proxies à frente da API — 1 atrás de um único proxy inverso. Por predefinição é 0, que ignora por completo o X-Forwarded-For. Esta é uma decisão com duas consequências: é também o que identifica os limites de pedidos de autenticação. Se for baixo demais, todos os clientes partilham o contador do proxy; se for alto demais, um cliente consegue forjar o seu próprio endereço. Mais vale começar baixo e medir.
Servir cada organização e equipa no seu próprio subdomínio é possível, mas vem desligado por predefinição. É regido por NESSIE_TEAM_HOST_BASE_DOMAIN: se não estiver definido, o encaminhamento por nome de anfitrião fica totalmente desativado, e o portão de emissão de certificados NESSIE_TLS_CHECK_KEY recusa tudo enquanto estiver por definir — que é a predefinição correta para uma implementação que não usa esta funcionalidade.
Verificações de estado
| Endpoint | Pergunta a que responde |
|---|---|
GET /api/health | Este processo está vivo? Só falha depois de iniciar o encerramento. |
GET /api/health/ready | Esta réplica pode receber novos pedidos? É esta que deve ser indicada ao balanceador de carga. |
GET /api/ops/health | Como está a funcionar a implementação? Um relatório para uma pessoa — sinais de atividade dos workers, profundidade da fila, tarefas falhadas — e não uma sonda. |
Ao receber SIGTERM, a API termina os pedidos em curso em vez de parar de imediato. NESSIE_SHUTDOWN_TIMEOUT_MS limita esse tempo e tem por predefinição 25 segundos; convém mantê-lo abaixo do período de graça que o orquestrador dá ao contentor, senão o runtime termina o processo primeiro e a conclusão dos pedidos não serve de nada.
Atualizar
Uma atualização é prisma migrate deploy sobre a base de dados existente, depois os dados iniciais e depois reconcile. São os mesmos quatro comandos da primeira implementação.
O caminho é testado para a rota que quem instala a solução por conta própria segue de facto, e não contra uma base de dados nova: a CI restaura uma cópia tirada vinte migrações atrás, migra-a para a versão mais recente, valida o esquema e executa uma verificação de funcionamento sobre as tabelas principais. Qualquer versão dentro dessa janela final é uma origem de atualização comprovada. As pastas de migração são imutáveis depois de confirmadas, e um passo de lint falha a compilação se alguma for renomeada, renumerada, eliminada ou alterada, porque qualquer uma destas operações quebra o migrate deploy numa base de dados que já registou a linha antiga.
Uma migração falhada bloqueia todas as implementações seguintes. O migrate deploy para na primeira falha e recusa todas as migrações posteriores com P3009, pelo que a instalação fica na versão anterior até que alguém a desbloqueie manualmente. A produção do próprio projeto passou um dia inteiro a recusar cada implementação desta forma. Para a desbloquear, é preciso confirmar que a migração falhada foi mesmo revertida — nenhum dos objetos que cria deve existir, e qualquer alteração deve manter a forma original — e só então a marcar como revertida. Uma migração aplicada apenas a meio não é candidata.
A cópia de segurança e a restauração da implementação Compose instalada pelo próprio utilizador não estão documentadas no repositório, por isso esta página não descreve um procedimento para isso.
Uma só máquina ou uma cloud gerida
O caminho de Docker Compose numa única máquina, descrito acima, é o único comprovado: é o que a produção do projeto executa, num único servidor, com a base de dados, o armazenamento de objetos, a API, o worker, a admin e o site como contentores atrás de um proxy inverso partilhado.
Escalar para além de uma máquina altera três coisas, todas elas impostas e não apenas aconselhadas. Fora do modo local, o Nessie executa a API e o worker como processos separados e em várias cópias de cada um, por isso qualquer capacidade que pressuponha que um processo é dono do disco da máquina é recusada: armazenamento de objetos em sistema de ficheiros (uma falha no arranque), modelos de ambiente de execução com o fornecedor docker (inoperantes, e o aprovisionamento falha de forma explícita) e as ferramentas incorporadas file_read, file_write e file_glob (um resultado de ferramenta falhado que o agente consegue ler). Cada recusa indica a definição, o modo e o que usar em alternativa.
Para uma cloud gerida, o repositório tem uma árvore Terraform em infrastructure/terraform/ dirigida ao Google Cloud: a API como serviço do Cloud Run com um mínimo de uma instância — mantém um cliente LISTEN persistente no Postgres e executa tarefas de manutenção periódicas, por isso reduzi-la a zero interrompe a entrega em tempo real — o worker como pool de workers do Cloud Run, porque não abre nenhuma porta e um Service nunca chegaria a ficar disponível; uma tarefa de migração que condiciona cada lançamento; o Cloud SQL para PostgreSQL 17; e um bucket do GCS acedido através do backend compatível com S3.
Esse caminho está escrito, mas não percorrido. O repositório diz isso claramente, e esta página também: ainda não existe nenhum projeto no Google Cloud para ele, nada na árvore foi planeado nem aplicado a um projeto real, o caminho de envio multiparte para o GCS não foi testado com um bucket real, o tipo de recurso do pool de workers não está validado, e o número de saltos de proxy de confiança é um valor provisório a medir, e não uma medição. A admin e o site público não estão modelados ali de todo. Deve tratar-se como um ponto de partida a verificar, e não como uma implementação suportada.
Onde ficam os dados de uma implementação, o que o software guarda e o que deliberadamente não guarda, é uma página à parte: Feito na UE, e onde ficam os dados.