decision-log
Dette dokumentet er auto-synket fra kildefilene i boligassistent-repoet. Endringer her vil overskrives ved neste sync. Rediger kildefilen direkte.
ADR-022: Admin API-token for maskin-til-maskin-tilgang til admin-endepunkter
Section titled “ADR-022: Admin API-token for maskin-til-maskin-tilgang til admin-endepunkter”Dato: 2026-04-29 Status: Besluttet
Kontekst
Claude Code må kunne lese og oppdatere produktfeedback i prod-DB-en som del av
/plan-phase, /implement-spec og /review-spec (Spec 076). Auth.js-sesjoner
krever interaktiv innlogging, CSRF-håndtering og cookie-juggling — ikke egnet for
ikke-interaktive shell-scripts. Direkte prod-DB-tilgang fra dev-maskin reverserer
ADR-021-separasjonen mellom dev og prod.
Alternativer vurdert:
- Auth.js-sesjon i scripts: krever email+passord i
.env, CSRF-token-handshake, cookie-jar i shell. Skjørt og sikkerhetsmessig like dårlig som token (passord er bredere skoped enn et formål-spesifikt token). - Per-bruker JWT/PAT: overkill for et 2-bruker privat prosjekt. Krever utstedelses-endepunkt, lagring per bruker, rotering per bruker.
- OAuth2 client credentials: samme overkill, krever IdP-oppsett.
- Direkte prod-DB-credentials: bryter ADR-021-separasjonen og sprer prod-tilgang bredere enn nødvendig.
Beslutning
Admin-endepunkter (/api/v1/admin/*) aksepterer enten session-cookie med
role: admin eller bearer-token via Authorization: Bearer <token>-header.
- Tokenet er én statisk verdi lagret som
ADMIN_API_TOKENenv-var på Render (encrypted secret) og samme verdi i lokal.env - Generert med
openssl rand -base64 48(entropi tilstrekkelig for stat. resistance) - Sammenlignes med
crypto.timingSafeEqualfor å unngå timing-leak - API-en kaster ved oppstart i
NODE_ENV=productionhvis env-var mangler (jf. engineering-standards seksjon 1: «Secrets — aldri fallback-verdier») - Tokenet brukes kun mot admin-endepunkter, ikke som generell user-auth-erstatning
- Rotasjon: kun ved kjent kompromittering — oppdater Render-env og lokal
.envsamtidig
Begrunnelse
- Statisk admin-token er enklest mulige løsning som dekker behovet for et 2-bruker privat prosjekt
- Bevarer ADR-021-separasjonen — ingen prod-DB-credentials på dev-maskin
- Auth.js-sesjon-pathen åpner for fremtidig admin-UI uten ekstra arbeid
- Konstanttidssammenligning beskytter mot timing-baserte angrep mot kort token-prefix
Konsekvenser
- Ny middleware
requireAdminTokenOrSessionipackages/auth/src/middleware.ts - Ny env-variabel
ADMIN_API_TOKEN(Render +.env+.env.example+turbo.jsoni build/dev/test-tasks) - Ny env-variabel
BOLIG_API_URL(defaulthttps://bolig.bomedai.com) for at scripts kan peke på lokalt API i dev - Bootstrap-validering i
apps/api/src/index.ts— API-en starter ikke i produksjon uten gyldig token - Tokenet logges aldri (bekreftes via Pino-redact-default — ingen
Authorizationi request-logging) - Rate-limiteren
apiLimiter(/api/v1) dekker disse endepunktene - Spec 076 er primær konsument; admin-UI som konsument er fremtidsrett, ikke i scope
Hva som ikke besluttes her (utsettes):
- Per-bruker tokens / PAT-utstedelse: utsatt til reelt behov oppstår (multi-bruker)
- Separat admin-CORS-policy: ikke aktuelt så lenge admin-API-er kun kalles serverside fra scripts
- Auditlogg av admin-handlinger: vurderes ved senere admin-funksjoner som faktisk endrer brukerdata (PATCH-feedback er trygt nok uten audit)
ADR-021: Hostingplattform — Render + Cloudflare R2 erstatter hjemme-PC + Cloudflare Tunnel + MinIO
Section titled “ADR-021: Hostingplattform — Render + Cloudflare R2 erstatter hjemme-PC + Cloudflare Tunnel + MinIO”Dato: 2026-04-27 Status: Besluttet
Kontekst
Applikasjonen kjørte på utviklers hjemme-PC via Docker Compose, eksponert gjennom
Cloudflare Tunnel til bolig.bomedai.com. Tjenesten er nede når PC-en er av eller
nettverket svikter, har ingen automatisk failover, ingen IaC, og avhenger fullstendig
av at én fysisk maskin er online. Skalering til 3–4 eiendommer × 2 brukere (6–8 brukere
totalt) krever en plattform med uptime-garantier og uten tidssensitive ad hoc-vedlikehold
fra utvikler.
Alternativer vurdert:
- Hetzner CX22 + Coolify/Dokploy (~€5/mnd): billigst, men krever Linux-administrasjon, manuell oppgradering av Postgres, OS-patching. Strider mot kravet om minimal sysadmin.
- Fly.io Machines + Neon Postgres (~$25/mnd): Docker-native, EU-region, men to separate leverandører å koordinere; mer initial-oppsett.
- Railway (~$20/mnd flat): docker-compose-importerer enkelt, men mindre moden Postgres-backup-historikk og svakere Postgres-tooling enn Render.
- Render (~$26/mnd): mest managed, automatisk Postgres-backup (7-dagers PITR inkludert i Basic-plan), automatisk SSL via Let’s Encrypt, auto-deploy fra GitHub, rollback med ett klikk. Beste match mot kravet om minimal sysadmin.
MinIO (AGPL-3.0) holdes som S3-kompatibelt lokalt i Docker Compose, men erstattes av Cloudflare R2 i produksjon. R2 har gratis egress og en free tier (10 GB lagring + 1M Class A-ops/mnd) som dekker bruk for 4 eiendommer komfortabelt.
Beslutning
-
Hostingplattform: Render Web Service (Starter $7) + Render PostgreSQL (Basic 1 GB, $19) + Cloudflare R2 (free tier). Total: ~$26/mnd hosting + uendret ~$2–12/mnd Gemini-API.
-
Kombinert Express-tjeneste i produksjon: Én container serverer både REST API (
/api/*) og den bygde React-SPAen viaexpress.static()med SPA-fallback. Dette eliminerer cross-origin cookie-problematikk (Auth.jssameSite='lax'fungerer naturlig same-origin) og holder antall services på Render til ett. -
Objektlagring: MinIO-klienten erstattes med
@aws-sdk/client-s3v3 +@aws-sdk/s3-request-presigner. Samme kode brukes lokalt mot MinIO og i prod mot R2 — kunS3_ENDPOINT-env varierer. URL-rewriting-blokken igetPresignedUrl()fjernes (R2-presignerte URL-er er direkte brukbare). -
DNS: Cloudflare DNS-only (grey cloud). CNAME
bolig.bomedai.com→<service>.onrender.com. Render håndterer Let’s Encrypt-cert. Cloudflare Tunnel ogcloudflared-containeren utfases. -
CI/CD: Auto-deploy fra
main-branch. Render leserrender.yamlBlueprint på rotnivå for deklarativ services-konfig.preDeployCommandkjører Drizzle-migrasjoner før hver swap. Helsesjekk på/healthblokkerer container-swap til ny build er klar. -
Bakgrunnsjobber: pg-boss kjører fortsatt i samme Express-prosess som API (uendret fra dagens oppsett). Splitt til dedikert Background Worker først ved 10+ brukere eller dokumentert CPU-saturering.
-
Backup: Render Postgres Basic inkluderer 7-dagers PITR. Daglig kryptert
pg_dumptil R2 settes opp som GitHub Actions-cron som ekstra sikkerhet.
Begrunnelse
- Renders Starter-tier har ingen sleep ved inaktivitet (kun Free-tier sover).
- Combined service eliminerer en hel klasse av cross-origin cookie- og CORS-problemer som ville oppstått med separate Static Site + API-services.
- R2 er S3-kompatibel: ingen kodemessig forskjell mot MinIO bortsett fra endpoint og presigning-mekanikken (som forenkles fordi URL-rewriting ikke trengs).
- Cloudflare DNS-only unngår orange clouds 100-sek HTTP-timeout som kan kutte AI-chat-streaming eller lange Gemini Files API-kall.
- Skalering: Standard-planen ($25) er ett-klikks-oppgradering uten nedetid når RAM eller CPU blir flaskehals — ikke premature optimization å starte på Standard.
Konsekvenser
- Nye filer:
Dockerfile.render(rot, multi-stage som bygger web + api),render.yaml(rot, Blueprint),apps/api/src/scripts/migrate.ts(kalt avpreDeployCommand). - Endrede filer:
apps/api/src/lib/minio.tsogservices/minio.service.tsomskrives til AWS SDK v3 (filnavn beholdes for diff-stabilitet).apps/api/src/index.tsfårexpress.static()+ SPA-fallback i produksjons-grenen.turbo.json,.env.example,docker-compose.ymlfår S3-vars som erstatter MINIO_-vars.packages/db/src/migrate.tsrefaktorert til å eksportererunMigrations-funksjon. - Beholdes uendret: Lokal
docker-compose.ymlmed MinIO + Postgres (kuncloudflared-tjenesten ogproduction-profilen fjernes).apps/web/Dockerfileogapps/web/nginx.confbrukes kun lokalt — ikke på Render. - Datamigrering: Eksisterende eiendomsdata, embeddings og bilder migreres
via
pg_dump/restore +rclone sync. Gjennomføres som planlagt nedetidsvindu med delta-sync ved cutover. - pgvector-aktivering: Render Postgres aktiverer ikke pgvector automatisk —
CREATE EXTENSION IF NOT EXISTS vector;må kjøres manuelt via Render PSQL-konsoll før første migrate (init.sql i Docker Compose kjøres ikke på managed Postgres). - Rollback-strategi: Lokal Docker-volume og MinIO-data beholdes som kald sikkerhetskopi i 14 dager etter cutover. CNAME kan flippes tilbake til Cloudflare Tunnel ved alvorlig regresjon.
- services-and-licensing.md oppdateres med Render Web Service, Render PostgreSQL og Cloudflare R2 i kostnadstabellen. MinIO-AGPL-blokken markeres som “kun lokal dev”.
Hva som ikke besluttes her (utsettes til senere):
- Migrasjon til Render Standard-plan ($25): trigges reaktivt ved 502-feil eller RAM > 80 % konsekvent.
- Cloudflare orange cloud (proxied): vurderes etter at SSE-streaming og lange AI-kall er bekreftet stabile på grey cloud.
- Splitt av pg-boss til Background Worker: utløses ved 10+ brukere eller dokumentert CPU-saturering.
- ISO 27001 / SOC 2 / EU-residency-krav: ikke aktuelt mens dette er privat bruk.
ADR-020: Samtalehukommelse — nøkkelfakta lagres i DB og injiseres i neste sesjon
Section titled “ADR-020: Samtalehukommelse — nøkkelfakta lagres i DB og injiseres i neste sesjon”Dato: 2026-04-17 Status: Besluttet
Kontekst
Brukere forventer at AI-assistenten husker viktige fakta på tvers av sesjoner — f.eks. at brukeren har allergier mot fuktprodukter, at badet er under planlegging eller at de foretrekker å gjøre enklere ting selv. Gemini 2.5 Flash har 1M token kontekstvindu, som er mer enn nok for én hel sesjon inkludert historikk og hentet kontekst. Men sesjoner er avgrenset — en ny sesjon starter uten hukommelse fra forrige.
Beslutning
Etter at en sesjon avsluttes (f.eks. når brukeren navigerer bort eller eksplisitt avslutter),
ber AI modellen identifisere opptil 10 nøkkelfakta fra samtalen som bør huskes. Disse skrives
til conversation_memory-tabellen. Ved neste sesjon hentes aktive minner og injiseres i
system-prompten som en kompakt blokk.
Minner utløper ikke automatisk, men kan deaktiveres (is_active = false) av bruker eller
av modellen (dersom et faktum er utdatert).
Begrunnelse
- Full historikk-injeksjon per sesjon: mulig men unødvendig — nøkkelfakta er mer presist og billigere
- Vektor-basert «long-term memory»: overkill for 2 brukere med fokusert brukshistorikk
- DB-lagring: enkel, testbar, reviderbar — ingen ekstra infrastruktur
Konsekvenser
- Ny tabell:
conversation_memory(se entities.md) - AI-endepunkt ber modellen om å generere minner etter siste assistent-tur i sesjonen
- System-prompt inkluderer
[HUKOMMELSE FRA TIDLIGERE SESJONER]-blokk - Minner er per bruker (ikke per eiendom) — to brukere på samme hus har separate minner
- Ingen automatisk utløp: minner akkumuleres over tid og filtreres aktivt
ADR-019: Tool use-mønster — 4 strukturerte verktøy, ikke ReAct-agent
Section titled “ADR-019: Tool use-mønster — 4 strukturerte verktøy, ikke ReAct-agent”Dato: 2026-04-17 Status: Besluttet
Kontekst
AI-assistenten trenger tilgang til live data fra boligdatabasen (observasjoner, vedlikeholdsplan, forbedringer, tilstandsavvik). Alternativene er: (a) ReAct-agent som iterativt kaller verktøy og resonnerer, eller (b) forhåndsdefinerte verktøy modellen kan kalle i én omgang.
Beslutning
4 forhåndsdefinerte verktøy eksponeres for AI-modellen via Gemini function calling:
| Verktøy | Input | Output |
|---|---|---|
søk_eiendomsdata | query, propertyId | Hybrid-søk (semantisk + full-tekst) over alle entiteter |
hent_vedlikehold | propertyId, sesong? | Vedlikeholdsplan med oppgaver og status |
hent_observasjoner | propertyId, romId? | Observasjoner filtrert per rom |
hent_forbedringer | propertyId, status? | Forbedringsidéer filtrert på status (vurderer/planlagt/kjøpt) |
Brave Search integreres som 5. verktøy (søk_eksternt) for reell-tid data (priser,
reguleringer, produkter) — dette er en separat integrasjon med egne env-variabler og
kostnadskonsekvenser (se ADR-019b når Brave Search implementeres).
Begrunnelse
- ReAct-agenter er uforutsigbare, vanskelige å teste og kan gå i uendelige resonnerings-løkker
- 4 begrensede verktøy gir kontrollerbar, testbar og deterministisk oppførsel
- Gemini function calling er stabilt og veldokumentert
- Brave Search skilles ut fordi det er en ekstern integrasjon med separate kostnader og env-vars
Konsekvenser
- Verktøy implementeres som TypeScript-funksjoner med Zod-validert input
- Modellen kaller verktøy i ett steg (ikke iterativt) — svar injiseres i neste prompt-tur
- Testene kan mocke verktøy-output og verifisere at modellen bruker resultatet korrekt
- Brave Search er separat spec (Spec 046) og separat env-var (
BRAVE_SEARCH_API_KEY)
ADR-018: Chat-arkitektur — SSE streaming, Gemini 2.5 Flash, alltid re-hent fra DB
Section titled “ADR-018: Chat-arkitektur — SSE streaming, Gemini 2.5 Flash, alltid re-hent fra DB”Dato: 2026-04-17 Status: Besluttet
Kontekst
AI-assistenten genererer svar som kan ta 5–30 sekunder. Brukere forventer progressivt svar (streaming), ikke et langt tomrom etterfulgt av fullstendig tekst. Parallelt er det risiko for «context poisoning» — at hallusinerte fakta fra tidlige assistent-turer akkumuleres og behandles som fakta i påfølgende turer.
Beslutning
- SSE (Server-Sent Events) som streaming-transport — ikke WebSocket
- Gemini 2.5 Flash (
gemini-2.5-flash) med 1M token kontekstvindu - Alltid re-hent fra DB per samtaletur — ikke stol på AI-omtalte fakta som kilde
- System-prompt skiller mellom
[FAKTUM FRA DB]og[MODELLVURDERING]
Begrunnelse
- SSE er HTTP-native: ingen ekstra infrastruktur (WebSocket krever upgrade-handshake og holder tilkobling oppe — unødvendig for request-response-mønsteret i en AI-chat)
- Gemini 2.5 Flash: #1 på mange benchmarks, 1M kontekstvindu tillater full samtalehistorikk uten å trunkere — eliminerer behov for strikse tur-limiter
- Re-hent fra DB: forhindrer at hallusinerte fakta akkumuleres (context poisoning). Modellen kan tro at “badet ble renovert i 2022” fordi den sa det i tur 3 — med DB-re-hent er det kun verifiserte fakta som kan siteres som sannhet
Konsekvenser
- Express SSE-endepunkt:
res.setHeader('Content-Type', 'text/event-stream'),res.write('data: ...\n\n')per chunk - Frontend:
EventSourceAPI ellerfetchmedReadableStream - Samtalehistorikk: siste N turer sendes med i hvert kall (Gemini 1M context = ingen streng grense)
- Tool-kall (ADR-019) kjøres i kontekst av DB-re-hent per tur, ikke mellomlagret i historikk
- Ingen WebSocket-infrastruktur i prosjektet (se services-and-licensing.md)
ADR-017: Søkestrategi — hybrid RRF (pgvector cosine + PostgreSQL full-tekst, norsk)
Section titled “ADR-017: Søkestrategi — hybrid RRF (pgvector cosine + PostgreSQL full-tekst, norsk)”Dato: 2026-04-17 Status: Besluttet
Kontekst
Semantisk søk alene fungerer dårlig for boligdata: modellnummer (NIBE S1255-16), TG-koder
(TG2), rom-IDer og norske tekniske termer er ikke representert godt i embedding-rommet.
Ren full-tekst-søk mister semantisk likhet (f.eks. «vannlekkasje» vs. «fuktskade»).
Beslutning
Hybrid søk kombinert med Reciprocal Rank Fusion (RRF):
- Semantisk søk: cosine similarity mot
content_embeddingsvia pgvector - Full-tekst søk:
to_tsquery+to_tsvectormed norsk Snowball-stemmer (norwegian) - RRF-kombinasjon:
score = 1/(k + rank_semantic) + 1/(k + rank_fulltext)(k=60)
Implementeres som én PostgreSQL-funksjon som returnerer rangerte resultater.
Begrunnelse
- Ingen ekstra infrastruktur: pgvector og PostgreSQL full-tekst er allerede tilgjengelig
- Norsk Snowball-stemmer er innebygd i PostgreSQL:
to_tsvector('norwegian', text) - RRF er enkel, robust og veldokumentert — ikke avhengig av hyperparameter-tuning
- Håndterer både modellnummer (full-tekst) og semantisk likhet (embedding) i ett kall
Konsekvenser
content_embeddings-tabell (ADR-016) er forutsetning- PostgreSQL-funksjon
hybrid_search(query, propertyId, limit)implementeres i Drizzle via SQL-tag søk_eiendomsdata-verktøyet (ADR-019) kaller denne funksjonentsv_indexpå relevante tekst-kolonner i eksisterende tabeller (Drizzle-migrasjoner)
ADR-016: Embedding-modell — gemini-embedding-001 med MRL, HNSW-indeks i pgvector
Section titled “ADR-016: Embedding-modell — gemini-embedding-001 med MRL, HNSW-indeks i pgvector”Dato: 2026-04-17 Status: Besluttet
Kontekst
Semantisk søk over boligdata krever en embedding-modell som håndterer norsk tekst med høy
kvalitet. text-embedding-004 (tidligere brukt i planlegging) ble deprecated januar 2026.
pgvector med 3072-dimensjonale vektorer er kostbart på disk og indeks-størrelse for de mengdene
vi snakker om (estimert <10K vektorer totalt for 2 brukere).
Beslutning
- Modell:
gemini-embedding-001(output_dimensionality=1536) - MRL (Matryoshka Representation Learning): trim fra 3072 → 1536 dims, gir <0.3% kvalitetstap
- Indeks: HNSW i pgvector (
m=16,ef_construction=64), 95%+ recall - Chunk-strategi: semantiske chunks per entitet (ikke fast window) — ett chunk per felt-gruppe
Begrunnelse
gemini-embedding-001er #1 MTEB Multilingual (68.32) og støtter norsk nativt- MRL-trim halverer vektor-størrelsen (fra 3072 til 1536) med minimalt kvalitetstap
- HNSW: ingen treningsfase (IVFFlat krever minst 100 vektorer per centroid og re-training ved datavekst), inkrementelle inserts, 95%+ recall
- Samme API-nøkkel (
GEMINI_API_KEY) — ingen ny avhengighet
Konsekvenser
- pgvector extension: allerede tilgjengelig i PostgreSQL-imaget
- Ny tabell:
content_embeddings(se entities.md) - Embedding-jobb: pg-boss-worker trigges etter CRUD-hendelser (create/update), ikke synkront
- Batch-jobb:
embed:allscript for å indeksere eksisterende data ved Fase 5-oppstart - Env-var: ingen ny (bruker
GEMINI_API_KEY) - Kostnad: ~$0.00004 per 1000 tokens — estimert <$0.01/mnd for løpende oppdateringer
ADR-015: CommonJS-modulkonfigurasjon og pnpm-deploy for Node.js produksjonsbygg
Section titled “ADR-015: CommonJS-modulkonfigurasjon og pnpm-deploy for Node.js produksjonsbygg”Dato: 2026-04-16 Status: Besluttet
Kontekst
Prosjektet brukte opprinnelig module: "ESNext" og moduleResolution: "bundler" i alle
TypeScript-konfigurasjoner — optimalisert for Vite og tsx-kjøring i dev. Dette fungerte
i utvikling, men feiler i produksjons-Docker-bygg av tre grunner:
-
moduleResolution: "bundler"genererer import-setninger uten.js-extension. Node.js ESM krever eksplisitte extensions (import './routes/index.js', ikke./routes/index). -
Workspace-pakker (
auth,db,types) eksporterte TypeScript-kildefiler (src/index.ts). Plainnode(utentsx) kan ikke laste.ts-filer direkte. -
pnpms symlink-basertenode_modulesfungerer ikke i Docker multi-stage builds — symlinker fra builder-stagen finnes ikke i runner-stagen.
Beslutning
-
CommonJS for alle server-side pakker:
module: "CommonJS",moduleResolution: "node"itsconfig.jsonforapps/api,packages/auth,packages/db,packages/types.moduleResolution: "node"er den eneste korrekte oppløsningen for Node.js runtime. -
dist/-exports i alle workspace-pakker: Alle
package.jsoni workspace-pakker eksporterer./dist/index.js, ikke TypeScript-kildefiler. -
pnpm deployfor Docker runner-stage: Brukerpnpm --filter @boligassistent/api deploy --prod /deployi builder-stagen. Dette lager en standalone-katalog med alle avhengigheter flattened — ingen symlinker, fungerer i isolert runner-stage. -
Ekskludering av ESM-only scripts:
src/migrate.tsbrukerimport.meta.url(ESM-only). Ekskludert frapackages/db/tsconfig.json— kjøres kun viatsx src/migrate.tsdirekte. -
X-Forwarded-Protofra Cloudflare: nginx skal sende$http_x_forwarded_proto(Cloudflares verdi) videre til Express, ikke regenerere verdien fra intern HTTP-skjema. Expresstrust proxy: 1+ Auth.js bruker denne headeren for å generere riktige HTTPS-redirects.
Konsekvenser
- Produksjonsbygg kompilerer og starter korrekt med
node dist/index.js tsx-kjøring i dev er uendret — bruker fortsattbundler-oppløsning via Vitemigrate.tskan ikke kompileres avtsc(ekskludert), men kjøres direkte viatsx- Auth.js genererer korrekte
https://-redirect-URLer bak Cloudflare Tunnel
ADR-014: Gemini Files API som standard for PDF-ekstraksjon
Section titled “ADR-014: Gemini Files API som standard for PDF-ekstraksjon”Dato: 2026-03-29 Status: Besluttet
Kontekst
Spec 007 (salgsoppgave-import) ekstraherte tekst fra PDF via pdf-parse og sendte tekstslice til Gemini
som en streng. En typisk salgsoppgave er 60–100 sider; tilstandsrapporter er 100–160 sider.
Teksttrunkering er ikke akseptabelt for høy-presisjon ekstraksjon (prinsipp: AI-funksjonalitet
skal ha få begrensninger og levere presisjon og kvalitet).
Spec 011 (tilstandsrapport-import) krever ekstraksjon av alle TG-avvik — et oppgave der visuell forståelse side for side er vesentlig bedre enn rå tekst.
Beslutning
Bruk GoogleAIFileManager (fra @google/generative-ai/server) som standard metode for
PDF-ekstraksjon. PDF-bufferen skrives til en midlertidig fil, lastes opp til Gemini Files API,
modellen kaller med fileData-referanse, og filen slettes fra Gemini etter ekstraksjon.
Samme mønster brukes for begge ekstraksjonspaths:
gemini.extractor.ts(salgsoppgave) — oppgradert fra tekst-slice til Files APItilstandsrapport-extractor.ts(tilstandsrapport) — ny fil, bruker Files API fra start
Begrunnelse
- Ingen tekstgrense: Gemini leser hele PDF-en som et visuelt dokument side for side
- Presisjon: visuell layout (tabeller, avsnitt, TG-markering) bevares
- Konsistens: ett mønster for begge ekstraktorer
- Personvern: filer slettes eksplisitt fra Gemini etter ekstraksjon
- Samme
GEMINI_API_KEYog SDK — ingen ny avhengighet
Konsekvenser
gemini.extractor.ts: signaturen endres fra(pdfText: string)til(pdfBuffer: Buffer, fileName: string)ingestion.worker.ts: senderbufferogfile_namedirekte til ekstraktor (ikke lengerpdfText)- Tmp-fil opprettes i
os.tmpdir()og slettes ifinally-blokk (lekker ikke) - Lagringstid på Gemini-siden: 48 timer (eksplisitt sletting gjøres uansett)
- Prising: ~$0.15 per salgsoppgave, ~$0.38 per tilstandsrapport (innenfor $2–10/mnd-budsjettet)
ADR-013: Bytt AI-leverandør fra Anthropic til Google Gemini
Section titled “ADR-013: Bytt AI-leverandør fra Anthropic til Google Gemini”Dato: 2026-03-18 Status: Besluttet
Kontekst
Spec 007-lite (Salgsoppgave-import) var planlagt med @anthropic-ai/sdk og ANTHROPIC_API_KEY.
Brukeren har ikke tilgang til Anthropic API-nøkkel, men har opprettet en Google Gemini API-nøkkel.
Beslutning
Bytt AI-leverandør for PDF-ekstraksjon fra Anthropic Claude til Google Gemini:
- SDK:
@anthropic-ai/sdk→@google/generative-ai(Apache 2.0-lisens) - Env-variabel:
ANTHROPIC_API_KEY→GEMINI_API_KEY - Modell:
gemini-2.0-flash(støtter structured JSON output viaresponseMimeType) - Structured output:
responseMimeType: 'application/json'+responseSchemaigenerationConfig
Begrunnelse
- Tilgjengelighet: brukeren har Gemini-nøkkel, ikke Anthropic
- Kostnad: Gemini 2.0 Flash er langt billigere (~$0.01 vs ~$0.25 per salgsoppgave-import)
- Kvalitet: tilstrekkelig for strukturert JSON-ekstraksjon fra norske salgsoppgaver
Konsekvenser
apps/api/package.json:@anthropic-ai/sdkfjernes,@google/generative-ailegges til.env.example:ANTHROPIC_API_KEY→GEMINI_API_KEYapps/api/CLAUDE.md: env-var-tabell oppdatertdocs/architecture/services-and-licensing.md: Claude API-seksjon erstattet med Gemini-seksjon- Ingen kodeendringer i
src/ennå — implementeres som del av Spec 007-lite - Prinsippet «AI foreslår → bruker godkjenner → system lagrer» (ADR-010) er uendret
ADR-012: Innføring av Building-entitet mellom Property og Floor
Section titled “ADR-012: Innføring av Building-entitet mellom Property og Floor”Dato: 2026-03-18 Status: Besluttet
Kontekst
Den opprinnelige modellen hadde Property som eneste toppentitet og blandet
tomt-konseptet (adresse, tomteareal, ownership) med bygningskonseptet (byggeår,
BRA, type). En bruker med hus, garasje og dukkehus på én tomt har behov for å
modellere tre separate bygninger — noe som ikke var mulig med én Property-entitet.
Endringen gjøres før Spec 001 implementeres, slik at vi unngår migrasjonsgjeld.
Beslutning
Innfør Building som ny entitet mellom Property og Floor.
Propertyrepresenterer tomten: adresse, tomteareal, eierformBuildingrepresenterer én fysisk bygning på tomten: byggeår, BRA, typeFloortilhørerBuilding(ikke direkteProperty)SupportSpacetilhørerBuilding(nullable)BuildingSystemtilhørerBuilding(ikkeProperty)OutdoorSpacetilhører fortsattProperty(det er tomtens uteområder)buildinglegges til som gyldig verdi i allelocation_ref_type-enums
Begrunnelse
Uten Building-entitet er det umulig å holde etasjer, rom, systemer og tilstand adskilt per bygning på en eiendom med flere bygninger. Endringen er minimal og ikke overingeniering — den løser et reelt, konkret brukerbehov uten å innføre unødvendig kompleksitet.
Konsekvens
BuildingSystem.property_id→BuildingSystem.building_idFloor.property_id+ nyttFloor.building_id(behold property_id for ytelse)SupportSpacefår nullablebuilding_id- DB-tabellene
houses→properties,house_permissions→property_permissions - Ny
buildings-tabell i DB-skjema - Spec 001 og Spec 002 oppdateres til å inkludere Building der relevant
ADR-011: Express 5 routing — ingen wildcards, Auth.js uten path-parameter
Section titled “ADR-011: Express 5 routing — ingen wildcards, Auth.js uten path-parameter”Dato: 2026-03-18 Status: Besluttet
Kontekst
Under Fase 0-verifikasjon feilet API-oppstart med PathError: Missing parameter name at index 11: /api/auth/*.
Express 5 bruker path-to-regexp v8 som bryter bakoverkompatibiliteten med Express 4s /*-wildcard-syntaks.
Forsøk på å bruke navngitt wildcard /*splat løste PathError, men Auth.js (@auth/express)
mottok da feil URL-format og returnerte 400 Bad request på alle auth-endepunkter.
Beslutning
Auth.js monteres uten wildcard:
app.use('/api/auth', ExpressAuth(authConfig))Generelt prinsipp for alle routes i Express 5: bruk app.use('/prefix', handler) for
sub-rute-matching — wildcards trengs ikke for dette. Navngitt wildcard (/*splat) brukes
kun når path-resten eksplisitt skal fanges og brukes i handleren.
Begrunnelse
app.use(path, handler) i Express matcher alle requests der URL starter med path,
uavhengig av hva som kommer etter. Dette er tilstrekkelig for montering av sub-applikasjoner
og third-party middleware som Auth.js.
Konsekvens
Alle fremtidige route-registreringer i apps/api følger dette mønsteret.
Se engineering-standards.md seksjon 9 for fullstendig oversikt over Express 5 breaking changes.
ADR-010: Ingestion Engine — design av onboarding-pipeline
Section titled “ADR-010: Ingestion Engine — design av onboarding-pipeline”Dato: 2026-03-17 Status: Besluttet
Kontekst
Brukerbehov B22–B27 beskriver et onboarding-behov: bruker laster opp salgsoppgave og
takstrapport, AI ekstraherer strukturerte data, bruker godkjenner før noe lagres.
Dokumentet “Brukerbehov ved oppstart” foreslo fire nye entiteter:
SourceDocument, ExtractionJob, ExtractedFact, ReviewItem.
Beslutninger
1. Ikke opprette SourceDocument — bruk eksisterende Document
Document finnes allerede med doc_type: takstrapport/salgsoppgave. Å lage en ny
SourceDocument-entitet ville skapt to overlappende “dokument”-konsepter.
I stedet: legg til ingestion_status-felt på Document.
2. ReviewItem slås inn i ExtractedFact
ReviewItem hadde kun to felt (extracted_fact, user_action). Disse legges direkte
på ExtractedFact (user_action, user_corrected_value, reviewed_at, reviewed_by).
Enklere skjema, ingen ekstra join, fullstendig audit-trail bevares.
3. ExtractedFact utvides med field_path og suggested_value
Forslaget manglet presisjon om hva faktumet gjelder. field_path (f.eks. rooms[0].area_m2)
og suggested_value (JSONB) gjør det mulig å mappe fakta direkte til entitetsfelter og
bygge en strukturert review-UI.
4. pg-boss som jobbkø — ikke Redis/BullMQ
Asynkron PDF-prosessering krever en jobbkø. pg-boss bruker eksisterende PostgreSQL
og krever ingen ny infrastruktur. BullMQ (Redis) ville lagt til ny tjeneste.
MIT-lisens. Ingen kostnader.
5. Plassering i roadmap: Spec 007, Fase 4 Ingestion Engine bygges etter Spec 001–006 fordi extraction-pipelinen mapper direkte til kjerneentitetene — de må være stabile definert først. Naturlig å bygge parallelt med AI-assistenten da begge bruker Claude API.
Begrunnelse for kritisk designprinsipp «AI foreslår → bruker godkjenner → system lagrer» er ufravikelig fordi:
- AI gjør feil — særlig på norske takstrapporter med ustrukturert layout
- Brukeren må ha tillit til at data er korrekt
- Feil data i kjernemodellen er verre enn ingen data (jf. prioritering i CLAUDE.md)
Kostnadsimplikasjon
Parsing av én takstrapport (60 sider ≈ 80 000 tokens) koster $0.25 ved Sonnet 4.6-priser.
Onboarding med 2–3 dokumenter totalt ≈ $0.50–0.75. Engangsbeløp, godt innenfor
prosjektets kostnadsprofil ($2–10/mnd). Ingen flagg nødvendig.
Konsekvenser
Documentfår nytt feltingestion_status- Spec 007 opprettes for Ingestion Engine
apps/api/src/ingestion/modul legges til monorepo-strukturenpg-bosslegges til som avhengighet (dokumentert i services-and-licensing.md)
ADR-009: ImprovementDependency fremfor separat Project-entitet
Section titled “ADR-009: ImprovementDependency fremfor separat Project-entitet”Dato: 2026-03-17 Status: Besluttet
Kontekst
Brukerbehov B20 (“Kan vi kombinere kjøkken og bad-oppussing?”) og B21 (“Hva bør vi IKKE gjøre nå?”)
krever strukturert modellering av avhengigheter mellom forbedringsprosjekter.
Et utkast (“Forslag utvidelse modell”) foreslo en ny Project-entitet med ProjectDependency.
Beslutning
Ikke innføre en separat Project-entitet. I stedet:
- Fjerne fritekst-feltet
dependenciesfraImprovementIdea - Legge til ny
ImprovementDependency-junction-tabell med strukturertdependency_type ImprovementIdeafungerer som “prosjekt” for større tiltak via kobling til Tasks og andre idéer
Begrunnelse
Projecti forslaget var nesten identisk medImprovementIdea— duplikat-problem- Å innføre en ny entitet ville kreve svar på: “Hva er forskjellen på et Project og en ImprovementIdea?”
ImprovementDependencygir den strukturerte avhengighetsmodelleringen som B20/B21 krever- Simpler modell er enklere å vedlikeholde og forklare til AI-assistenten
Konsekvenser
ImprovementIdea.dependencies(fritekst) fjernes — erstattes avImprovementDependency- Spec 005 (ImprovementIdeas og Tasks) utvides til å inkludere
ImprovementDependency - AI-assistenten kan nå resonere om sekvens og samkjøring basert på strukturerte data
ADR-008: ProductCandidate slås sammen med InteriorAsset(considering)
Section titled “ADR-008: ProductCandidate slås sammen med InteriorAsset(considering)”Dato: 2026-03-17 Status: Besluttet
Kontekst
Brukerbehov B15 og B16 (produktsøk og produktvurdering) krever at systemet kan
evaluere et produkt mot romstørrelse, designretning og eksisterende møbler.
Et utkast (“Forslag utvidelse modell”) foreslo en separat ProductCandidate-entitet
i tillegg til eksisterende InteriorAsset og ProductReference.
Beslutning
Ikke innføre ProductCandidate som separat entitet. I stedet:
- Utvide
InteriorAssetmed felteneprice_nok,fits_room,matches_design InteriorAssetmedstatus=consideringer “produktkandidat”ProductReference(enkel lenkestump) beholdes for ustrukturerte referanserShoppingTracker(prissporing) utsettes til Post-MVP og behandles separat
Begrunnelse
- Tre overlappende entiteter (
ProductReference,InteriorAsset,ProductCandidate) ville gitt tvetydig livssyklus og duplisert data InteriorAssetmedstatus=consideringer allerede en “produktkandidat” — bare mangler noen evalueringsfelt- Livssyklusen blir tydelig: considering → owned/removed/planned
- Enklere for AI-assistenten å arbeide med én entitetstype
Konsekvenser
InteriorAssetfår nye felt:price_nok,fits_room,matches_designroom_idgjøres nullable — en vurdert sofa trenger ikke rom-tilknytning ennåShoppingTracker(prissporing med cron-jobb) er Post-MVP og ikke en del av MVP-scopeLightingPlan(belysningsplanlegging) er Post-MVP — krever Brave Search + LightingPlan-entitet
ADR-001: Relasjonsdatabase (PostgreSQL) fremfor grafdatabase
Section titled “ADR-001: Relasjonsdatabase (PostgreSQL) fremfor grafdatabase”Dato: 2026-03-17 Status: Besluttet
Kontekst Domenemodellen er naturlig en property graph med mange relasjoner på kryss og tvers. Grafdatabaser (Neo4j, Amazon Neptune) ville modellert dette naturlig.
Beslutning Bruke PostgreSQL med fremmednøkler og polymorfiske referanser, ikke en grafdatabase.
Begrunnelse
- PostgreSQL er velprøvd, godt dokumentert og eies av utviklere med SQL-kompetanse
- pgvector gir semantisk søk uten ekstern vector store
- Drizzle ORM gir typesikker tilgang uten overhead
- Relasjonene i dette systemet er ikke så komplekse at graftraversal er nødvendig
- Enklere drift og backup — én database, ikke to systemer
- Grafdatabase kan introduseres senere dersom spørremønsteret krever det
Konsekvenser
Polymorfiske referanser (location_ref_type + location_ref_id) brukes der én entitet
kan peke til flere typer. Dette gir noe svakere databaseintegritet, men er akseptabelt
for dette bruksområdet.
ADR-002: Issue og ImprovementIdea som separate entiteter
Section titled “ADR-002: Issue og ImprovementIdea som separate entiteter”Dato: 2026-03-17 Status: Besluttet
Kontekst Begge entiteter representerer «ting som bør gjøres med boligen». Man kunne modellert dem som én tabell med en type-enum.
Beslutning Holde Issue og ImprovementIdea som helt separate entiteter med separate tabeller.
Begrunnelse
- Issue er et faktum (noe er galt) — ImprovementIdea er et ønske (noe kan bli bedre)
- De har ulike felter: Issue har TG-kode og severity, ImprovementIdea har motivasjon og backlog-status
- Sammenblanding ville gjort prioritering og rapportering vanskeligere
- AI-assistenten trenger å skille mellom «hva er galt» og «hva ønsker vi»
Konsekvenser
Task kan peke til enten et Issue (via source_issue_id) eller en ImprovementIdea
(via source_improvement_id). Begge felter kan være null.
ADR-003: Equipment og InteriorAsset som separate entiteter
Section titled “ADR-003: Equipment og InteriorAsset som separate entiteter”Dato: 2026-03-17 Status: Besluttet
Kontekst Begge er fysiske objekter i et rom. Man kunne brukt én generisk «asset»-tabell.
Beslutning Equipment (teknisk utstyr) og InteriorAsset (møbler, dekor) er separate entiteter.
Begrunnelse
- Equipment tilhører et BuildingSystem og har vedlikeholdsbehov
- InteriorAsset tilhører DesignLayer og har stilattributter
- En varmepumpe og en sofa har fundamentalt ulike datamodeller
- AI-kontekst for «interiørrådgivning» skal ikke inkludere teknisk utstyr
Konsekvenser Ingen. Begge har egne tabeller og egne relasjoner.
ADR-004: DesignDirection som reviderbar, ikke historisk
Section titled “ADR-004: DesignDirection som reviderbar, ikke historisk”Dato: 2026-03-17 Status: Besluttet
Kontekst Designretning endrer seg over tid. Spørsmålet er om man skal bevare historikk (alle versjoner) eller bare nåværende tilstand.
Beslutning
DesignDirection har en status-enum (draft / active / revised / archived) og
updated_at, men ingen full versjonshistorikk i MVP.
Begrunnelse
- Full versjonshistorikk er kompleksitet som ikke gir verdi i MVP
- Status-feltet gir nok kontekst til å forstå at retningen er endret
- Dersom historikk viser seg viktig kan event sourcing legges til senere
Konsekvenser Historikk må dokumenteres manuelt (f.eks. som DecisionNote) dersom det er ønskelig.
ADR-005: Repo-basert prosjektminne og spec-driven development
Section titled “ADR-005: Repo-basert prosjektminne og spec-driven development”Dato: 2026-03-17 Status: Besluttet
Kontekst For å sikre at Claude Code jobber kontrollert og konsistent trengs et system for prosjektminne, specs og arbeidsstyring.
Beslutning Bruke filbasert spec-driven development med dokumenter i repoet som eneste sannhetskilde. CLAUDE.md definerer arbeidsregler. Specs definerer krav. Evaluations dokumenterer verifikasjon.
Begrunnelse
- Claude Code leser filer direkte — filbasert minne er mer pålitelig enn instruksjoner i minnet
- Specs i repoet versjonskontrolleres og kan revideres som kode
- Strukturert arbeidsflyt reduserer risiko for at Claude tar uønskede beslutninger
- Gir brukeren full oversikt og kontroll over hva som implementeres
Konsekvenser Alt arbeid skal gå via spec → task → implementasjon → verifikasjon. Ingen ad-hoc implementasjon uten spec-grunnlag.
ADR-006: Polymorfiske referanser for stedskobling
Section titled “ADR-006: Polymorfiske referanser for stedskobling”Dato: 2026-03-17 Status: Besluttet
Kontekst Mange entiteter (Issue, Observation, Task, SafetyItem) kan knyttes til ulike typer steder: Room, OutdoorSpace, SupportSpace, BuildingSystem. Alternativet er separate fremmednøkkelkolonner for hver type.
Beslutning
Bruke to-kolonne polymorfisk referanse: location_ref_type TEXT + location_ref_id UUID.
Begrunnelse
- Separate fremmednøkler (room_id, outdoor_space_id, …) gir mange nullable kolonner
- Polymorfisk referanse er enklere å lese og vedlikeholde
- Svakhet: ingen databasemessig fremmednøkkel-integritet — løses med applikasjonsvalidering
Konsekvenser
Applikasjonstesting må verifisere at location_ref_id faktisk peker på en eksisterende
entitet av riktig type. Legg til valideringslogikk i service-laget.
ADR-007: pgvector for AI-kontekst fremfor ekstern vector store
Section titled “ADR-007: pgvector for AI-kontekst fremfor ekstern vector store”Dato: 2026-03-17 Status: Besluttet
Kontekst AI-assistenten trenger semantisk søk for å finne relevante entiteter. Alternativer: Pinecone, Weaviate, Qdrant (eksterne tjenester), eller pgvector (PostgreSQL-utvidelse).
Beslutning Bruke pgvector som PostgreSQL-utvidelse.
Begrunnelse
- Holder alt i én database — enklere drift og backup
- Ingen ekstern avhengighet i MVP
- Cosine similarity-søk i pgvector er tilstrekkelig for dette bruksomfanget
- Kan migreres til ekstern vector store later uten store kodeendringer
Konsekvenser
Embeddings lagres som vector(1536) kolonner på relevante tabeller.
Embedding-oppdatering må kjøres ved create og update av relevante entiteter.