Gå til innholdet

tech-architecture

Dette dokumentet er auto-synket fra kildefilene i boligassistent-repoet. Endringer her vil overskrives ved neste sync. Rediger kildefilen direkte.

Monorepo gir felles typer, delt konfigurasjon og effektiv bygging. pnpm er raskere og mer diskeffektiv enn npm/yarn. Turborepo cacher builds og kjører tasks parallelt.

React er en moden, fleksibel UI-plattform med stort økosystem. Vite gir rask utvikling og hot reload. shadcn/ui gir ferdige, tilpassbare komponenter uten lock-in — komponentene kopieres inn i prosjektet og eies fullt ut.

Enkel og veldokumentert Node.js-ramme. Gir full kontroll over routing, middleware og API-design uten magi. Passer for et prosjekt der utvikleren ønsker å forstå hva som skjer.

Express 5 — breaking changes fra v4 (kjente gotchas):

Express 5 er en major versjon med breaking changes. De viktigste for dette prosjektet:

  • Routing wildcards: path-to-regexp v8 tillater ikke bare /*. Bruk eksplisitt path-prefix (app.use('/prefix', handler)) eller navngitt wildcard (/*splat). Se engineering-standards.md seksjon 9 for detaljer.
  • Auth.js-montering: app.use('/api/auth', ExpressAuth(config))ikke '/api/auth/*'. app.use med prefix-path håndterer sub-ruter automatisk.
  • Async handlers: Express 5 håndterer async route handlers native — ingen express-async-errors-pakke nødvendig.
  • server.close() callback: Er sync — ikke async. Bruk Promise-chaining inne i callback.

PostgreSQL er den mest fullverdige åpen kildekode-databasen og støtter pgvector som utvidelse — viktig for semantisk søk. Drizzle ORM gir typesikker spørring, migrations og full SQL-kontroll uten overhead fra tyngre ORM-er som Prisma.

Nødvendig for AI-kontekstbygging. Lagrer vektor-embeddings av entiteter (rom, avvik, møbler) slik at AI-assistenten kan finne semantisk relevante data til et brukerspørsmål via cosine similarity-søk. Alternativet ville vært ekstern vector store (Pinecone, Weaviate), men pgvector holder alt i én database.

Gir fleksibel autentisering med e-post/passord og potensielt sosiale logins uten ekstern avhengighet. Self-hosted betyr full kontroll over brukerdata. OAuth-providere kan legges til uten arkitekturendring.

Cloudflare R2 (produksjon) + MinIO (lokal dev)

Section titled “Cloudflare R2 (produksjon) + MinIO (lokal dev)”

S3-kompatibel objektlagring. To miljøer, én kodebase:

  • Lokal dev: MinIO i Docker Compose-container (port 9000) — ingen ekstern avhengighet
  • Produksjon: Cloudflare R2 (free tier dekker 10 GB lagring + ubegrenset egress)

API-koden bruker @aws-sdk/client-s3 v3 mot begge — kun S3_ENDPOINT-env varierer. Presignerte URL-er genereres med @aws-sdk/s3-request-presigner og leveres direkte til frontend (ingen URL-rewriting nødvendig).

Migrasjon mellom S3-leverandører (R2 → AWS S3 → Backblaze) krever kun env-vars-endring. Jf. ADR-021.

Render kjører Express + bygd React-SPA fra ett Docker-image (Dockerfile.render på rotnivå). Render Postgres Basic 1 GB hoster databasen med pgvector-extension og pg-boss-schema. Auto-deploy fra main-branch trigget via render.yaml Blueprint på rotnivå.

Kombinert Web + API i ett service eliminerer cross-origin cookie-problematikk (Auth.js sameSite='lax' cookies fungerer naturlig same-origin).

Alternativene som ble vurdert (Hetzner CX22 + Coolify, Fly.io + Neon, Railway) er dokumentert i ADR-021. Render valgt fordi det krever minst sysadmin-tid for privat bruk.

Gemini 2.5 Flash er pinnet modell-ID. Brukes til både PDF-ekstraksjon (Files API, ADR-014) og AI-chat med tool use (ADR-019). Støtter strukturert JSON-output via responseMimeType + responseSchema. Pris: ~$0.075–0.15 per 1M input-tokens, $0.30 per 1M output-tokens. Estimert månedlig kostnad ved daglig bruk: $2–12.

Brukes for romvisualisering. Ikke i MVP, men arkitekturelt planlagt. Generering av bilder basert på stilbeskrivelser og rombeskrivelser.

Gir nettbasert produktsøk uten avhengighet til Google. Brukes av AI-assistenten til å finne konkrete møbler og produkter basert på stil og funksjon. Ikke i MVP.


boligassistent/
├── apps/
│ ├── api/ — Express.js backend
│ │ ├── src/
│ │ │ ├── routes/ — HTTP route handlers (tynne)
│ │ │ ├── services/ — Forretningslogikk
│ │ │ ├── db/ — Drizzle-klient og migrasjoner
│ │ │ ├── ai/ — Claude og DALL-E integrasjoner
│ │ │ └── ingestion/ — Ingestion Engine (PDF-parsing, ekstraksjon, jobbkø)
│ │ └── Dockerfile
│ └── web/ — React + Vite frontend
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── hooks/
│ │ └── lib/
│ └── Dockerfile
├── packages/
│ ├── types/ — Delte TypeScript-typer
│ ├── auth/ — Auth.js-konfigurasjon
│ └── db/ — Drizzle-skjema og migrations
├── docker-compose.yml
├── turbo.json
└── pnpm-workspace.yaml

En AI-assistent som gir generiske råd er lite nyttig. For å gi kontekstuell rådgivning om én spesifikk bolig trenger Claude tilgang til riktige data om den boligen.

Løsning: Strukturert kontekstbygging + pgvector RAG

Section titled “Løsning: Strukturert kontekstbygging + pgvector RAG”

Steg 1 — Strukturert kontekst (alltid med) Når brukeren stiller et spørsmål, bygges en strukturert kontekstblokk:

Eiendom: Hellebakken 9, enebolig, byggeår 1975
Valgt rom: Stue (34 m², 1. etasje)
Designretning interiør: nordisk, naturlig, varm
LayoutIntent stue: salong + arbeidshjørne, behov for mykt lys

Steg 2 — Semantisk søk (pgvector) Brukerens spørsmål embeddes og søkes mot:

  • Rom-beskrivelser
  • Issue-tekster
  • Observation-tekster
  • Manualinnhold (chunked)
  • DesignDirection-tekster

De mest relevante entitetene inkluderes i konteksten.

Steg 3 — Prompt til Claude Kontekstblokk + relevante data + brukerens spørsmål sendes til Claude API. Claude svarer basert på faktiske data om boligen, ikke generiske råd.

  • Entiteter embeddes ved opprettelse og oppdatering
  • Embedding-modell: text-embedding-3-small (OpenAI) eller tilsvarende
  • Embeddings lagres i embedding vector(1536) kolonne på relevante tabeller


Gjør det mulig å onboarde en ny bolig på 5–10 minutter ved å laste opp salgsoppgave og/eller takstrapport. AI ekstraherer strukturerte data, bruker godkjenner, systemet lagrer.

Kritisk prinsipp: AI fyller aldri databasen direkte. Flyten er alltid: AI foreslår → bruker godkjenner → system lagrer.

1. Upload — Bruker laster opp PDF → lagres i MinIO → Document opprettet
2. Queue — ExtractionJob opprettes med status=queued
3. Parse — pdf-parse ekstraherer tekst fra PDF (side-for-side)
4. Extract — Claude API med structured output identifiserer fakta:
rom, arealer, systemer, avvik, tilstandsgrader
5. Structure — Fakta mappes til entitets-felt via field_path
6. Store — ExtractedFact-rader lagres med status=pending
7. Review — Bruker ser gjennom fakta, godkjenner/endrer/forkaster
8. Persist — Godkjente fakta skrives til de ordinære tabellene
KomponentValgBegrunnelse
PDF-tekstekstraksjonpdf-parse (MIT)Enkel, veldokumentert, ingen ekstern avhengighet
Jobbkøpg-boss (MIT)Bruker eksisterende PostgreSQL, ingen ny infrastruktur
AI-ekstraksjonClaude API med tool use / structured outputGir typesikre JSON-responser
Gjennomgangs-UIReact + shadcn/uiInline redigering, diff-visning, godkjennknapper

Pipelinen sender dokumenttekst i bolker og ber Claude returnere strukturert JSON:

// Forenklet eksempel på extraction-prompt
const systemPrompt = `
Du er en ekspert på norske takstrapporter og salgsoppgaver.
Ekstraher strukturerte data og returner dem som JSON etter dette skjemaet: [schema]
For hvert faktum, oppgi: verdi, sidenummer, tekstutdrag, confidence (0.0-1.0).
Sett lav confidence på usikre tolkninger.
`

En takstrapport på 60 sider ≈ 80 000 tokens → ~$0.25 per dokument (Sonnet 4.6). Onboarding med 2–3 dokumenter totalt ≈ $0.50–0.75 (engangsbeløp).


Produksjonsoppsett (nåværende, fra 2026-04-27)

Section titled “Produksjonsoppsett (nåværende, fra 2026-04-27)”

Applikasjonen kjører på Render (managed PaaS) og er tilgjengelig via https://bolig.bomedai.com. Cloudflare brukes kun til DNS (grey cloud, DNS-only). Jf. ADR-021 for begrunnelse og alternativene som ble vurdert.

Internett → Cloudflare DNS (CNAME) → Render edge (TLS-terminering) → Express
├── /api/auth/* Auth.js
├── /api/v1/* REST
├── /health Render health-check
├── pg-boss-workers (in-process)
└── express.static() → React SPA dist
┌────────────────────┬─────────────────────┐
▼ ▼ ▼
Render Postgres Cloudflare R2 Gemini API
(Basic 1GB, $19) (free tier, S3) (Vertex/AI Studio)
├── pgvector (HNSW) └── 32 obj, 167 MB
└── pg-boss schema

Render-ressurser (deklarert i render.yaml Blueprint på rotnivå):

RessursPlanPris/mndRolle
Web ServiceStarter$7Combined Express + React SPA, autoscaler ikke brukt (single instans)
Postgres-databaseBasic 1 GB$19Primær DB med pgvector + pg-boss schema, 7-dagers PITR inkludert
Custom DomainInkludert$0bolig.bomedai.com med Let’s Encrypt-cert utstedt og fornyet av Render
Auto-deploy$0Trigget på push til main, kjører node dist/scripts/migrate.js som preDeploy

Cloudflare R2 (objektlagring, jf. ADR-021): Free tier dekker 10 GB lagring + 1M Class A-ops/mnd. Egress er gratis. Bucket: boligassistent-prod. API-credentials lagres som sync: false i Render.

Kritiske konfigurasjonsdetaljer:

  • Dockerfile.render (rot): multi-stage som bygger både web og api, runner-stagen kombinerer api-deploy + web-dist slik at Express serverer alt fra ett prosess.
  • apps/api/src/index.ts: express.static() + SPA-fallback (regex /^(?!\/api(\/|$)).*$/) i produksjon — same-origin gjør at Auth.js sameSite='lax' cookies fungerer naturlig.
  • Helmet CSP utvidet til img-src 'self' data: https: for å tillate eksterne presignerte R2-URL-er, og connect-src 'self' https: for fremtidige eksterne tjenester.
  • apps/api/src/lib/minio.ts: bruker @aws-sdk/client-s3 v3 mot både MinIO (lokalt) og R2 (prod) — kun S3_ENDPOINT-env varierer mellom miljøene.
  • pgvector-extension må aktiveres manuelt på Render Postgres første gang (CREATE EXTENSION IF NOT EXISTS vector;) — Render kjører ikke docker/postgres/init.sql.
  • apps/web/public/sw.js: service worker v2 som hopper over cross-origin GETs (R2-bilder) for å unngå at en kortvarig nettverksfeil cacher bilde som permanent FAILED.

Deploy-flyt:

git push origin main
▼ (auto-deploy via render.yaml)
Render bygger Dockerfile.render (~5-10 min)
├─ preDeployCommand: node dist/scripts/migrate.js
├─ Helsesjekk: GET /health må returnere 200
└─ Rolling swap (zero-downtime)
Live på https://bolig.bomedai.com

Render-spesifikke miljøvariabler (definert i render.yaml, secrets settes i Render-UI):

VariabelVerdi
NODE_ENVproduction
DATABASE_URLauto-injisert fra Render Postgres
AUTH_SECRETRender-generert ved første deploy
AUTH_URL / ALLOWED_ORIGINShttps://bolig.bomedai.com
S3_ENDPOINThttps://<account-id>.r2.cloudflarestorage.com
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEYR2 API-token fra Cloudflare
S3_BUCKETboligassistent-prod
GEMINI_API_KEY / OPENAI_API_KEY / BRAVE_SEARCH_API_KEYAPI-nøkler

Backup:

  • Render Postgres Basic: 7-dagers PITR inkludert
  • Daglig kryptert pg_dump til R2 via GitHub Actions (.github/workflows/backup.yml)
  • Monthly verified restore-test: ikke automatisert ennå, men kan kjøres manuelt

Lokal dev kjører fortsatt mot docker-compose.yml (postgres + minio) med pnpm dev. Vite serverer SPA på https://localhost:5173 (HTTPS via basicSsl-plugin), Express på port 3001. Vite-proxyen (changeOrigin: false + xfwd: true) bevarer Host-header slik at Auth.js trustHost-logikk genererer redirects til Vite-origin.

Dockerfile.render brukes ikke lokalt — den er kun for Render-deploy.

BrukereTiltak
6–8Oppgrader Web Service Starter → Standard ($25, 1 vCPU / 2 GB RAM)
10+Splitt pg-boss til dedikert Background Worker-service (samme repo, alternativ entrypoint)
> 4 eiend.Oppgrader Postgres Basic 1 GB → Standard 4 GB ($95) hvis embedding-volum vokser kraftig
EU-residency-kravBytt Render-region til frankfurt; revurder R2-jurisdiction (eu-bucket)