nessie

Nessie installeren

Draai Nessie op infrastructuur die je zelf kiest, van één machine tot een beheerde cloud.

Wat je nodig hebt voordat je begint

Nessie is een pnpm-monorepo met Node-services en twee statische bundels. Hoe je het ook draait, dezelfde vier dingen moeten er eerst zijn.

  • PostgreSQL met pgvector. Niet optioneel en niet te vervangen. De migraties voeren CREATE EXTENSION vector uit en maken kolommen vector(1024) aan. Productie draait op pgvector/pgvector:pg17.
  • Node 22 en pnpm. Elk Dockerfile in infrastructure/docker/ bouwt op node:22-slim, en de repository legt in de root-package.json vast welke package manager er wordt gebruikt (pnpm@10.22.0).
  • Een OpenAI-compatibel modelendpoint en een sleutel ervoor. Agents en embeddings lopen allebei hierlangs. Zie “Inference instellen” hieronder.
  • S3-compatibele objectopslag, voor elke deployment die niet de lokale ontwikkelomgeving is. Uploads op de lokale schijf worden geweigerd buiten de local-modus, en die weigering breekt het opstarten af in plaats van dat de service half werkend opstart — zie “Eén machine, of een beheerde cloud”.

Docker en een reverse proxy zijn nodig voor het containerpad dat hieronder wordt beschreven, en een domein plus TLS voor alles wat vanaf het internet bereikbaar is.

Hardwareafmetingen — minimale CPU, geheugen en schijfruimte voor een bepaald aantal mensen — staan nergens in de repository, dus deze pagina vermeldt ze ook niet.

De componenten en hoe ze samenwerken

ComponentWat het isIngress
api/De Fastify HTTP/WebSocket-API. Elke client praat met deze en met niets anders.Ja
worker/Agentorkestratie en achtergrondruns. Pollt de taakwachtrij; bindt geen poort.Nee
admin/De productinterface — een statische bundel, gebouwd met Vite en geserveerd door nginx.Ja
web/De publieke site. Statisch, optioneel en los van het draaien van het product.Ja
PostgreSQLDe database, de taakwachtrij en de realtime bus, alle drie.Nee
ObjectopslagBijlagen, avatars en kennisbankbestanden. S3-compatibel; in productie MinIO.Nee
gateway/Een optionele pushrelay voor APNs en FCM. Staat uit tenzij je hem bewust aanzet.Ja

De API en de worker zijn één image. De twee pakketten zijn sterk met elkaar verweven, dus Dockerfile.app bouwt het hele team één keer, en de workercontainer overschrijft het commando met node worker/dist/index.js. In de local-modus is er helemaal geen workercontainer: de API draait de worker ingebed in zijn eigen proces.

Er is geen Redis. De taakwachtrij en het realtime-transport draaien allebei op Postgres. Dat is een bewuste keuze, geen vergeten onderdeel — de Google Cloud-structuur schrapt om dezelfde reden de Pub/Sub- en Redis-modules.

Poorten

In lokale ontwikkeling staan de API op 5454 en de admin op 5455, standaard. Een checkout kan een eigen paar kiezen door NESSIE_API_PORT en NESSIE_ADMIN_PORT in te stellen, in de omgeving of in de .env van de repository; elke tool in de repository, de browsertestsuites inbegrepen, leest dezelfde twee waarden. Kies het paar vóór het starten en houd het aan: een draaiende service naar een andere poort verplaatsen laat de admin met de verkeerde API praten.

Productie is anders en valt niet onder die regel. De API-container luistert intern op 5554 (vastgezet met NESSIE_API_PORT) en de pushrelay op 5556, beide achter de reverse proxy.

Lokaal draaien op één machine, voor ontwikkeling

De repository bevat een Compose-bestand met alleen een database erin. Dat is alles wat de lokale loop nodig heeft.

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

Die container publiceert Postgres op 55432, met gebruikersnaam, wachtwoord en database allemaal nessie. Zet de verbindingsstring in een .env in de root van de repository — het dev-script van de API leest die — en pas daarna de migraties toe en bouw de worker één keer, omdat de API in de local-modus de worker laadt uit zijn gebouwde dist:

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

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

pnpm dev draait de API en de admin parallel met hot reload, op 5454 en 5455. Controleer of de API antwoordt met GET /api/health.

Postgres, en de pgvector-vereiste

Drie extensies worden door de migraties zelf aangemaakt — vector, pg_trgm en pgcrypto — dus de rol die ze uitvoert moet extensies mogen aanmaken. Op een beheerde Postgres is dat het eerste wat je controleert.

De vectorbreedte is onderdeel van het schema, geen instelling. thoughts.embedding, thought_recalls.query_embedding en knowledge_page_chunks.embedding zijn vector(N)-kolommen, en N wordt één keer vastgelegd als EMBEDDING_DIMENSIONS in packages/schemas/src/embedding.ts — momenteel 1024, de native breedte van het embeddingmodel dat productie gebruikt. Elk embeddingverzoek stuurt die breedte mee, dus een provider die met een andere breedte antwoordt, faalt hard in plaats van vectoren weg te schrijven die de database weigert. Overstappen naar een embeddingmodel met een andere breedte betekent die constante aanpassen, een migratie schrijven die de drie kolommen opnieuw typeert, en opnieuw embedden: vectoren van verschillende breedtes zijn niet om te zetten, dus de migratie maakt ze leeg (null) in plaats van ze af te kappen.

Als je Nessie naast andere applicaties op een gedeeld Docker-netwerk draait, geef de databasecontainer een eigen naam. De Compose-service van het project heet nessie-postgres en nooit postgres, omdat de servicenaam op het gedeelde netwerk een DNS-alias wordt, en een botsing zorgt ervoor dat postgres:5432 het verkeer afwisselend over twee databases verdeelt.

Configuratie

De runtimeconfiguratie is gelaagd: een nessie.config.json-bestand dat alleen-lezen aan de API en de worker is gekoppeld, met omgevingsvariabelen daarbovenop. De koppeling tussen die twee is ConfigEnvMap in packages/config, en dat is de gezaghebbende lijst.

NESSIE_MODE kiest de vorm van de deployment: local (de standaard, en het ontwikkelpad), hosted of selfHosted — wat een zelf gehoste installatie instelt. Het schakelt dev-login uit en vereist een CORS-allowlist.

Dit zijn de instellingen waarzonder een selfHosted-deployment niet start, of niet werkt:

VariabeleWaarom
DATABASE_URL / NESSIE_DB_URLDe Postgres-verbindingsstring.
NESSIE_AUTH_SECRET32 bytes hex (openssl rand -hex 32). Ondertekent sessies en bootstraptokens. Het opstarten faalt zonder dit buiten de local-modus, en het moet op elke replica en over alle deployments heen dezelfde waarde zijn, anders sterft elke sessie bij elke release.
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSIONEen JSON-sleutelring en het label dat de actieve root kiest; verplicht buiten de local-modus. Dit is een aparte root van het ondertekengeheim — de ring versleutelt elk duurzaam geheim dat Nessie opslaat. Gebruik de ene niet als de andere.
NESSIE_STORAGE_PROVIDERMoet s3 zijn. filesystem is de standaard en wordt geweigerd in de hosted- en selfHosted-modus, zodat een deployment die hier niets instelt niet start, in plaats van uploads naar een schijf te schrijven die geen andere container kan lezen.
NESSIE_CORS_ORIGINSDe admin-origin. De admin en de API zijn aparte origins, en de API moet de origin die hij serveert op de allowlist zetten.
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URLGebruikt om OAuth-redirect-URI’s te maken buiten een HTTP-verzoek. De API gooit een fout op het moment van een verzoek zonder de eerste.
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEYWaar inference naartoe gaat. Zie hieronder.

infrastructure/compose/.env.prod.example heeft precies de vorm van alle instellingen, met de optionele blokken — objectopslag, secret vault, SSO, gekoppelde mail — erin uitgecommentarieerd op hun plek. De volledige catalogus, inclusief elke optionele instelling, staat in docs/deployment/configuration.md.

Deployen met Docker Compose

Kopieer .env.prod.example naar .env naast docker-compose.prod.yml en vul het in. Start daarna de database, bouw, migreer en start de services. De volgorde is belangrijk: de migraties en de seeds draaien tegen een database die al draait, voordat een service iets serveert.

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

De productie-Dockerfiles draaien de lint van elk pakket voordat ze het bouwen, dus een lintfout is een buildfout.

De reconcile-stap is niet optioneel

reconcile zaait het standaardbeleid van elke organisatie en vult de tooltoekenningen voor beschermde MCP en de Personal Assistant aan. De API deed dit vroeger bij het opstarten op elke replica; nu verbindt hij, luistert hij en doet hij verder niets. Daarom geldt: zonder deze stap heeft de eerste organisatie geen beleidsregels, en wordt elke koppeling van een agent geweigerd. De stap is idempotent, dus draaien bij elke deploy kost niets — en een upgrade die met de hand is toegepast, moet hem ook draaien.

De eerste eigenaaraccount

Een nieuwe selfHosted-installatie heeft geen gebruikers, dus de API maakt een eenmalig bootstraptoken aan en logt een instel-URL. Het token staat in Postgres en niet in een proces, zodat elke replica het eens is.

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

Open https://<your-admin-host>/bootstrap?token=<token> en maak de eigenaaraccount aan. Het token verloopt na 15 minuten; herstart de API-service om een nieuw token te laten aanmaken.

Als je SSO instelt, is er helemaal geen bootstrapstap. De bootstrapmodus wordt uitgeschakeld zodra een externe authenticatieprovider aanstaat — anders zou die het inlogscherm kapen — en de eerste persoon die via SSO inlogt, zet het standaardteam op en wordt de eigenaar ervan.

Inference instellen

Chat is OpenAI-compatibel. NESSIE_MODEL_BASE_URL is het endpoint, NESSIE_MODEL_API_KEY de bearer, en NESSIE_MODEL_PROVIDER noemt de adapter. Nessie levert adapters voor openai, kimi en deepseek, en bereikt al het andere via de generieke OpenAI-compatibele connector. NESSIE_MODEL_NAME stelt het standaardmodel van de deployment in, waar de modelkeuze van een afzonderlijke agent nog steeds voorrang op heeft.

Embeddings worden apart gerouteerd, omdat de chatprovider ze mogelijk helemaal niet aanbiedt — DeepSeek bijvoorbeeld heeft geen embeddingendpoint. Elk NESSIE_EMBEDDING_*-veld dat je niet instelt, erft de chatprovider, dus een deployment die er geen instelt, embedt via het chatendpoint. Stel NESSIE_EMBEDDING_PROVIDER, NESSIE_EMBEDDING_MODEL en, bij een proxy die op service routeert, het servicesegment in wanneer de chatprovider niet kan embedden. Dit verkeerd instellen faalt stil in plaats van luid: geheugenherkenning en kennisbankzoekopdrachten vallen terug op een lexicaal kanaal en gaan door, en loggen intussen kb_search: query embedding failed.

De eigen kostenadministratie van Nessie — de tokenregistratie per organisatie en de budgetpoort — wordt niet beïnvloed door het endpoint waarnaar je wijst, en werkt hetzelfde, of er nu ondertekende toewijzing per aanroep is ingesteld of niet.

Inloggen

Nessie authenticeert mensen via UnlikeOtherAI, dat ook de organisatie- en teamstructuur beheert. De inlogpagina van de admin toont één knop: “Inloggen / Registreren”.

De integratie is een config-JWT-flow en geen standaard OIDC: de API serveert een ondertekend RS256-configuratiedocument op GET /api/auth/sso/config en de bijbehorende JWKS op GET /.well-known/jwks.json, beide op de API-host. Het opzetten gebeurt eenmalig in een vaste reeks: genereer een RSA-2048-sleutelpaar en stel UOA_CONFIG_JWT_PRIVATE_KEY_B64, UOA_CONFIG_JWT_KID, UOA_DOMAIN, UOA_CONFIG_URL, UOA_JWKS_URL, UOA_REDIRECT_URL en UOA_CONTACT_EMAIL in; klik één keer op de knop, wat een integratieverzoek aanmaakt; laat dat goedkeuren, wat een eenmalige link naar het contactadres stuurt met daarin het clientgeheim; stel UOA_CLIENT_SECRET in en herstart de API. De kid moet uniek zijn per domein.

Achter een domein zetten, met TLS

Een deployment serveert drie namen: de publieke site, de admin en de API. De admin- en API-origins zijn niet inwisselbaar. Dockerfile.admin bakt VITE_API_BASE_URL tijdens het bouwen in de bundel, dus elk gebouwd admin-artefact — inclusief een desktopbuild die het insluit — moet tegen de API-origin worden gebouwd. Bouw je het tegen de eigen origin van de admin, dan wordt /api/auth/providers omgezet naar de HTML-shell van de admin, en blijft het inloggen hangen op “Providers laden...”.

Productie beëindigt TLS bij Caddy, dat per hostnaam certificaten ophaalt via HTTP-01 en daarvoor geen DNS-providerplugin nodig heeft. Richt de proxy op de admin- en webcontainers op poort 80 en op de API op de interne poort.

Stel NESSIE_API_TRUSTED_PROXY_HOPS in op het aantal proxy’s voor de API — 1 achter één reverse proxy. De standaard is 0, wat X-Forwarded-For volledig negeert. Dit is één beslissing met twee gevolgen: het bepaalt ook hoe de authenticatie-ratelimieten worden gegroepeerd. Te laag betekent dat elke client de bucket van de proxy deelt; te hoog betekent dat een client zijn eigen adres kan vervalsen. Kies liever laag en meet het.

Elke organisatie en elk team op een eigen subdomein serveren kan, en staat standaard uit. Het wordt bepaald door NESSIE_TEAM_HOST_BASE_DOMAIN. Als die niet is ingesteld, worden tenants helemaal niet op hostnaam gerouteerd, en de poort voor certificaatuitgifte NESSIE_TLS_CHECK_KEY weigert alles zolang hij niet is ingesteld — dat is de juiste standaard voor een deployment die die functie niet gebruikt.

Healthchecks

EndpointVraag die het beantwoordt
GET /api/healthLeeft dit proces nog? Faalt alleen zodra het is begonnen met afbouwen.
GET /api/health/readyMag deze replica nieuwe verzoeken aannemen? Dit is degene waarop je een load balancer richt.
GET /api/ops/healthHoe gaat het met de deployment? Een rapport voor een persoon — workerheartbeats, wachtrijdiepte, dode taken — geen probe.

Bij SIGTERM bouwt de API af in plaats van op de plek te sterven. NESSIE_SHUTDOWN_TIMEOUT_MS begrenst die afbouw en staat standaard op 25 seconden. Hou hem onder de graceperiode die je orchestrator de container geeft, anders beëindigt de runtime het proces eerst en heeft de afbouw geen zin.

Upgraden

Een upgrade is prisma migrate deploy tegen de bestaande database, daarna de seeds, daarna reconcile. Dezelfde vier commando’s als bij de eerste deploy.

Het pad wordt getest voor de route die een zelfhoster echt neemt, niet tegen een verse database: CI herstelt een snapshot die twintig migraties achterloopt, migreert die naar de huidige head, valideert het schema en draait een smoketest over de kerntabellen. Elke release binnen dat staartvenster is een bewezen upgradebron. Migratiemappen zijn onveranderlijk zodra ze zijn gecommit, en een lintstap laat de build falen als er één wordt hernoemd, hernummerd, verwijderd of gewijzigd, omdat alle vier migrate deploy breken voor een database die de oude rij al heeft geregistreerd.

Eén mislukte migratie blokkeert elke deploy erna. migrate deploy stopt bij de eerste fout en weigert elke latere migratie met P3009, dus de installatie blijft op de oude release totdat iemand dat met de hand opheft. De eigen productie van het project heeft een dag lang elke deploy zien afwijzen. Opheffen betekent bevestigen dat de mislukte migratie echt is teruggedraaid — geen van de objecten die ze aanmaakt mag bestaan, en alles wat ze wijzigt moet nog in de oorspronkelijke vorm zijn — en pas daarna als teruggedraaid markeren. Een migratie die half is toegepast komt niet in aanmerking.

Back-up en herstel voor de zelf gehoste Compose-deployment staan niet in de repository gedocumenteerd, dus deze pagina beschrijft er geen procedure voor.

Eén machine, of een beheerde cloud

Het Docker Compose-pad op één host hierboven is het bewezen pad: het is wat de eigen productie van het project draait, op één server, met de database, objectopslag, API, worker, admin en site als containers achter een gedeelde reverse proxy.

Opschalen over meer dan één machine verandert drie dingen, en die worden alle drie afgedwongen in plaats van geadviseerd. Buiten de local-modus draait Nessie de API en de worker als aparte processen, en als meerdere kopieën van elk. Daarom wordt elke functie die ervan uitgaat dat één proces de schijf van de machine bezit, geweigerd: bestandssysteem-objectopslag (een fout bij het opstarten), uitvoeringsomgevingssjablonen met de docker-provider (zonder effect, en provisioning faalt hard), en de ingebouwde tools file_read, file_write en file_glob (een mislukt toolresultaat dat de agent kan lezen). Elke weigering noemt de instelling, de modus en wat je in plaats daarvan moet gebruiken.

Voor een beheerde cloud bevat de repository een Terraform-structuur in infrastructure/terraform/ die op Google Cloud is gericht: de API als Cloud Run-service met een minimum van één instantie — hij houdt een persistente Postgres-LISTEN-client open en draait onderhoudssweeps, dus opschalen naar nul stopt de realtime levering — de worker als Cloud Run-workerpool, omdat hij geen poort bindt en een Service nooit klaar zou zijn; een migratiejob die elke uitrol bewaakt; Cloud SQL voor PostgreSQL 17; en een GCS-bucket die via de S3-compatibele backend wordt bereikt.

Dat pad is geschreven, maar nog niet doorlopen. De repository zegt dat zonder omwegen, en deze pagina ook: er bestaat nog geen Google Cloud-project voor, niets in de structuur is gepland of toegepast tegen een echt project, het multipart-uploadpad naar GCS is niet getest tegen een echte bucket, het type workerpool is niet gevalideerd, en het aantal vertrouwde proxyhops daarin is een placeholder die nog gemeten moet worden in plaats van een meting. De admin en de publieke site zijn daar helemaal niet gemodelleerd. Behandel het als een startpunt dat je zelf verifieert, niet als een ondersteunde deployment.

Waar de data van een deployment staat, wat de software bewaart en wat ze bewust niet bewaart, staat op een aparte pagina: Made in de EU, en waar je data staat.