tech-architecture
Dette dokumentet er auto-synket fra kildefilene i boligassistent-repoet. Endringer her vil overskrives ved neste sync. Rediger kildefilen direkte.
Begrunnelse per komponent
Section titled “Begrunnelse per komponent”Turborepo + pnpm
Section titled “Turborepo + pnpm”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 + Vite
Section titled “React + Vite”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.
Express.js
Section titled “Express.js”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-regexpv8 tillater ikke bare/*. Bruk eksplisitt path-prefix (app.use('/prefix', handler)) eller navngitt wildcard (/*splat). Seengineering-standards.mdseksjon 9 for detaljer. - Auth.js-montering:
app.use('/api/auth', ExpressAuth(config))— ikke'/api/auth/*'.app.usemed 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 + Drizzle ORM
Section titled “PostgreSQL + Drizzle ORM”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.
pgvector
Section titled “pgvector”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.
Auth.js (self-hosted)
Section titled “Auth.js (self-hosted)”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 PaaS (produksjons-hosting)
Section titled “Render PaaS (produksjons-hosting)”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.
Google Gemini API
Section titled “Google Gemini API”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.
DALL-E 3
Section titled “DALL-E 3”Brukes for romvisualisering. Ikke i MVP, men arkitekturelt planlagt. Generering av bilder basert på stilbeskrivelser og rombeskrivelser.
Brave Search API
Section titled “Brave Search API”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.
Monorepo-struktur
Section titled “Monorepo-struktur”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.yamlAI-arkitektur og kontekstbygging
Section titled “AI-arkitektur og kontekstbygging”Problem
Section titled “Problem”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 1975Valgt rom: Stue (34 m², 1. etasje)Designretning interiør: nordisk, naturlig, varmLayoutIntent stue: salong + arbeidshjørne, behov for mykt lysSteg 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.
Embedding-strategi
Section titled “Embedding-strategi”- 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
Ingestion Engine
Section titled “Ingestion Engine”Formål
Section titled “Formål”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.
Pipeline
Section titled “Pipeline”1. Upload — Bruker laster opp PDF → lagres i MinIO → Document opprettet2. Queue — ExtractionJob opprettes med status=queued3. Parse — pdf-parse ekstraherer tekst fra PDF (side-for-side)4. Extract — Claude API med structured output identifiserer fakta: rom, arealer, systemer, avvik, tilstandsgrader5. Structure — Fakta mappes til entitets-felt via field_path6. Store — ExtractedFact-rader lagres med status=pending7. Review — Bruker ser gjennom fakta, godkjenner/endrer/forkaster8. Persist — Godkjente fakta skrives til de ordinære tabelleneTekniske komponenter
Section titled “Tekniske komponenter”| Komponent | Valg | Begrunnelse |
|---|---|---|
| PDF-tekstekstraksjon | pdf-parse (MIT) | Enkel, veldokumentert, ingen ekstern avhengighet |
| Jobbkø | pg-boss (MIT) | Bruker eksisterende PostgreSQL, ingen ny infrastruktur |
| AI-ekstraksjon | Claude API med tool use / structured output | Gir typesikre JSON-responser |
| Gjennomgangs-UI | React + shadcn/ui | Inline redigering, diff-visning, godkjennknapper |
Claude API-bruk i ekstraksjon
Section titled “Claude API-bruk i ekstraksjon”Pipelinen sender dokumenttekst i bolker og ber Claude returnere strukturert JSON:
// Forenklet eksempel på extraction-promptconst 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.`Kostnad
Section titled “Kostnad”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).
Deployment
Section titled “Deployment”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 schemaRender-ressurser (deklarert i render.yaml Blueprint på rotnivå):
| Ressurs | Plan | Pris/mnd | Rolle |
|---|---|---|---|
| Web Service | Starter | $7 | Combined Express + React SPA, autoscaler ikke brukt (single instans) |
| Postgres-database | Basic 1 GB | $19 | Primær DB med pgvector + pg-boss schema, 7-dagers PITR inkludert |
| Custom Domain | Inkludert | $0 | bolig.bomedai.com med Let’s Encrypt-cert utstedt og fornyet av Render |
| Auto-deploy | — | $0 | Trigget 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.jssameSite='lax'cookies fungerer naturlig.- Helmet CSP utvidet til
img-src 'self' data: https:for å tillate eksterne presignerte R2-URL-er, ogconnect-src 'self' https:for fremtidige eksterne tjenester. apps/api/src/lib/minio.ts: bruker@aws-sdk/client-s3v3 mot både MinIO (lokalt) og R2 (prod) — kunS3_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 ikkedocker/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.comRender-spesifikke miljøvariabler (definert i render.yaml, secrets settes i Render-UI):
| Variabel | Verdi |
|---|---|
NODE_ENV | production |
DATABASE_URL | auto-injisert fra Render Postgres |
AUTH_SECRET | Render-generert ved første deploy |
AUTH_URL / ALLOWED_ORIGINS | https://bolig.bomedai.com |
S3_ENDPOINT | https://<account-id>.r2.cloudflarestorage.com |
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | R2 API-token fra Cloudflare |
S3_BUCKET | boligassistent-prod |
GEMINI_API_KEY / OPENAI_API_KEY / BRAVE_SEARCH_API_KEY | API-nøkler |
Backup:
- Render Postgres Basic: 7-dagers PITR inkludert
- Daglig kryptert
pg_dumptil R2 via GitHub Actions (.github/workflows/backup.yml) - Monthly verified restore-test: ikke automatisert ennå, men kan kjøres manuelt
Lokal utvikling
Section titled “Lokal utvikling”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.
Skalering — fremtidige steg
Section titled “Skalering — fremtidige steg”| Brukere | Tiltak |
|---|---|
| 6–8 | Oppgrader 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-krav | Bytt Render-region til frankfurt; revurder R2-jurisdiction (eu-bucket) |