Over dit project

De boot houdt zelf de wacht. De cloud maakt het op afstand begrijpelijk.

Bootwacht bewaakt kleinere zeilboten tijdens winterstalling en aan de steiger. Aan boord hangt een Raspberry Pi met Zigbee-sensoren; die slaat elke meting eerst lokaal op een SSD op en stuurt hem daarna versleuteld naar één Next.js-applicatie in de EU. Deze pagina legt het hele systeem uit — van de I²C-draadjes tot de transactionele notificatie-outbox — voor wie meebouwt, meekijkt of overweegt klant te worden.

onderdelen
22
routes
6
ADR-besluiten
8
datamodellen
18
Bekijk de repository

Deze hele pagina als één tekstbestand — handig om in NotebookLM, een LLM of een e-reader te plakken.

Dit is een uitleg, geen contract. De repository, de ADR's en de contractdocumenten zijn leidend zodra deze pagina en de code van elkaar afwijken. Bootwacht is bovendien een informatiesysteem: het vervangt geen verzekering, gecertificeerd alarmsysteem of onderhoud. Laatst herzien: 2026-08-15.

Uitgangspunten

Zes keuzes die alles verklaren

01

Lokaal eerst, altijd

Elke waarneming wordt op de boot naar SQLite geschreven vóórdat er ook maar een netwerkverbinding wordt geprobeerd. Valt 4G weg, dan blijft de historie compleet en levert de boot later alsnog na. De SSD aan boord bewaart standaard 3.650 dagen; de cloud 90.

02

Alleen uitgaande verbindingen

De Raspberry Pi begint élke verbinding zelf — ook voor schakelopdrachten, die hij ophaalt in plaats van ontvangt. Er is dus geen open poort, geen VPN-endpoint en geen publiek bereikbare Home Assistant. Dat werkt door carrier-grade NAT heen en houdt het aanvalsoppervlak van een onbemande boot minimaal.

03

Bewust klein gehouden

Eén VPS, één applicatieproces, één SQLite-bestand, geen betaalde platformabonnementen. De pilot van winter 2026/27 moet begrijpelijk en goedkoop zijn; de schaalgrens is expliciet opgeschreven zodat niemand er per ongeluk overheen loopt.

04

De interface liegt niet

Een gevraagde toestand is geen bevestigde toestand. Het dashboard toont pas 'aan' als de Pi de opdracht heeft uitgevoerd en de werkelijke stand heeft teruggemeld, en markeert verbruiksdata na twee uur als verouderd. Offline, mislukt en onbekend blijven zichtbaar.

05

Privacy zit in het model

Of iets openbaar mag zijn, staat op de metricdefinitie — niet in een view. Locatie is standaard privé; de publieke Idefix-pagina is één expliciet geauditeerde uitzondering op precies één punt van precies één boot, zonder historie.

06

Open source en zelf uit te breiden

Standaardhardware (Raspberry Pi, Zigbee, Home Assistant) en een publieke repository in plaats van een gesloten merkdoos. Een eigenaar kan zelf een sensor bijprikken: een nieuwe numerieke of booleaanse metric wordt bij eerste levering automatisch geregistreerd.

Systeemkaart

Drie omgevingen, één product

De architectuur scheidt bewust wat er aan boord draait, wat centraal gehost wordt en wat Bootwacht helemaal niet zelf doet.

Klik een onderdeel voor de details, of kies hieronder een route om te zien wat er meedoet.

Op iedere bewaakte boot

Aan boord

Sensoren, een Zigbee-coördinator, Home Assistant, de Bootwacht Edge-service en een eigen SQLite-database op SSD. Dit deel blijft werken zonder internet.

Eén VPS in de EU

In de cloud

Eén Next.js-proces achter een reverse proxy: publieke site, telemetrie-API, commandowachtrij, alarmlogica, notificatie-outbox en drie soorten dashboards.

Mensen en diensten van derden

Buiten Bootwacht

Browsers van klanten en beheerders, plus de diensten die Bootwacht bewust niet zelf bouwt: betalen, e-mail versturen, kaarten tonen, operationele monitoring en back-ups bewaren.

Volg een route door het systeem

Werkend

De belangrijkste route van het systeem. Let op de volgorde: eerst lokaal vastleggen, dan pas het netwerk proberen.

Stap 1 van 7

De sensor meet

Een SNZB-02D meldt 4,1 °C over Zigbee; de INA219 aan de I²C-bus meldt 12,4 V. Batterijgevoede sensoren slapen tussen berichten door, wat jaren batterijduur oplevert.

Betrokken onderdelen:

Verder lezen:Telemetriecontract v1Telemetrie-domeinmodel

Technologiekeuzes

Wat elke keuze oplevert — en wat ze kost

Elk onderdeel heeft één smalle taak. Klap iets open voor de reden en het bijbehorende compromis.

01

Aan boord: meten en storingen overleven

Alles op de Pi is gekozen op één criterium: blijft het werken als er drie weken niemand aan boord komt en de 4G-verbinding wegvalt?

Raspberry Pi 4

De computer aan boord, gevoed vanaf de 12 V-serviceaccu via een 5 V-omvormer.

Genoeg rekenkracht voor Home Assistant plus een container, met een enorme hoeveelheid bestaande documentatie en onderdelen. Aandachtspunt: de hub is een single point of failure en vraagt een watchdog, herstartstrategie en een doordacht laagspanningsgedrag.

Zigbee 3.0

Draadloos netwerk met laag verbruik tussen de sensoren en de coördinator aan boord.

Water-, contact-, beweging- en temperatuursensoren zijn breed verkrijgbaar en goedkoop, en slapende batterijsensoren zijn een volwassen techniek. LoRaWAN is uitgesteld, niet afgewezen: dat wordt pas interessant bij 'Bootwacht Marina', waar veel boten één gateway kunnen delen. Matter lost interoperabiliteit op, niet de verbinding met de wal.

Home Assistant

Hardware-integratielaag met een stabiele lokale API.

Het kent duizenden apparaten, zodat Bootwacht geen eigen driverlaag hoeft te onderhouden en een eigenaar zelf een sensor kan bijprikken. Bootwacht behandelt het bewust als een leverancier: alleen de ondersteunde API, nooit de interne database.

Python

De taal van de Bootwacht Edge-service.

Een kleine, expliciete en goed testbare service naast Home Assistant, dat zelf ook Python is. Sluit bovendien aan op het analysewerk dat de maker in Python doet.

SQLite

De duurzame opslag aan boord, op de SSD.

Eén bestand, transacties, checks en foreign keys, zonder serverproces of netwerkafhankelijkheid. Precies wat je wil in een omgeving waar de stroom er zomaar af kan gaan.

Docker

Herhaalbare runtime voor Edge en Home Assistant.

Identieke Python-dependencies op elke Pi, plus harde inperking: geen capabilities, read-only rootfilesystem en schrijfrecht op alleen het datavolume.

02

In de cloud: mensen én apparaten bedienen

Eén applicatie bedient drie heel verschillende doelgroepen: browsers van klanten, beheerschermen en een vloot apparaten die geen mens achter zich heeft.

Next.js15 (App Router)

Website, dashboards en alle server-endpoints in één project.

Django is serieus overwogen — het geeft gratis een sterke backoffice en past bij het Python-werk van de maker. Toch werd Next.js gekozen: het wint op PWA en dashboardinteractiviteit, houdt het bij één taal end-to-end, en de site bestond al geaudit en gedocumenteerd. De erkende prijs: beheerschermen bouw je met de hand.

React19

De interactieve onderdelen van dashboards en beheer.

Server components voor alles wat mag worden voorgerenderd, client components alleen waar echt interactie nodig is. Dat houdt privélogica op de server.

TypeScript

Eén getypeerde taal van database tot knop.

Prisma genereert types uit het schema, dus een modelwijziging breekt bij het typechecken en niet pas in productie. npm run typecheck is verplicht vóór oplevering.

Prisma6

Schema, migraties en getypeerde queries.

Eén schemabestand is de bron voor database én types, en $transaction geeft de atomaire alarm-plus-outboxregel die het notificatieontwerp nodig heeft.

Tailwind CSS3

Huisstijl en responsive opmaak.

De maritieme kleuren staan als tokens in tailwind.config.ts: navy voor rust, 'signal' als messing scheepsbeslag, zeildoek-zand als achtergrond en reddingsboei-koraal voor acties. Koppen in Fraunces, tekst in Source Sans.

jose + bcryptjs

Wachtwoordhashing en ondertekende sessies.

Geen extern auth-platform voor twee rollen en een handvol gebruikers. bcrypt hasht, jose ondertekent met een vastgepind HS256, en de databasecontrole op tokenVersion levert de intrekbaarheid die een pure JWT mist.

web-push (VAPID)

Standaard Web Push zonder tussenpartij.

Zelfbeheerde VAPID-sleutels betekenen geen abonnement, geen extra verwerker en geen leveranciers-lock-in — passend bij een pilot die klein en uitlegbaar moet blijven.

Plotly (basic dist)

De uitgebreide grafiekverkenner over de hele retentieperiode.

Zoomen en pannen op langere reeksen. De compacte 48-uurs sparklines zijn juist handgeschreven SVG, zodat het dashboard zelf licht blijft.

03

Hosting en diensten van derden

De pilot draait op één VPS in de EU, en de dingen die Bootwacht niet zelf wil zijn — betaalverwerker, mailprovider, kaartleverancier — zijn bewust uitbesteed.

Ubuntu 24.04 LTS + Node.js 22

Besturingssysteem en runtime op de VPS.

Langdurige ondersteuning en de meest gangbare combinatie voor een gebouwde Next.js-applicatie. De app draait als een eigen onbevoorrechte servicegebruiker.

Caddy

HTTPS-ingang, certificaatbeheer en client-IP-waarheid.

Automatische certificaten en een korte, leesbare configuratie. Let op: bij een Plesk-image gebruik je de Nginx/Apache-ingang van Plesk — twee producten mogen niet vechten om poort 80 en 443.

systemd

Procesbewaking, geplande taken en logging.

Start na een reboot, herstart na een crash en activeert persistente oneshot-timers voor offlinebewaking, windverwachtingen en notificatiebezorging. Journalctl maakt uitkomsten zichtbaar; geen extra procesmanager of betaalde scheduler nodig.

STRATO VPS Linux S

De pilotinfrastructuur: 2 vCPU, 2 GB RAM, 90 GB NVMe.

Vast, laag maandbedrag zonder kosten per request, bij de partij waar het domein al staat. Vercel is uitgesteld omdat SQLite en een ratelimiter in procesgeheugen niet passen bij stateless meerdere instanties. Back-ups zitten níét in het pakket.

Stripe Checkout

Gehoste betaalpagina met iDEAL en kaart.

Kaartgegevens komen nooit langs Bootwacht, dus de PCI-scope blijft minimaal. Mollie zou een vervanging van drie bestanden zijn: stripe.ts, /api/checkout en de webhook.

Resend

Aflevering van contactformulierberichten.

Eenvoudige API en domeinverificatie. Orderbevestigingen en wachtwoordreset zijn bewust nog niet gebouwd en blijft afzonderlijk gepland in #2.

Domeinmodel

Van boot tot losse meetwaarde

De cloud slaat genormaliseerde observaties op in plaats van één steeds bredere tabel met een kolom per denkbare sensor.

identityUser

Account met rol CUSTOMER of ADMIN. tokenVersion is de knop om sessies in te trekken.

Velden

  • email String @unique
  • passwordHash String— bcrypt
  • role String— CUSTOMER | ADMIN
  • tokenVersion Int— ophogen = alle sessies van deze gebruiker direct ongeldig

Relaties

  • bezit 0..n Boat
  • heeft 0..n Order
  • heeft 0..n PushSubscription
  • heeft 0..n DashboardReportPeriod
fleetBoat

De aggregate root. Alles hangt aan een boot, en een boot mag al bestaan en meten vóórdat er een klant aan gekoppeld is.

Velden

  • name String
  • marina String?
  • deviceToken String @unique— crypto.randomBytes, bewust géén cuid — die zijn voorspelbaar
  • isPublic Boolean— maximaal één boot tegelijk; de admin-API zet de rest uit
  • userId String?— onDelete: SetNull

Relaties

  • heeft 0..n Sensor, TelemetryObservation, Alert
  • heeft 0..n BoatAlertSetting en AlertRuleState
  • heeft 0..n ControllableOutlet
  • heeft 0..n DashboardReportPeriod
  • heeft 0..1 BoatDataUsageReset
fleetDashboardReportPeriod

Het klantgebonden startmoment van het totaalrapport. Een reset verschuift alleen dit tijdstip en laat telemetrie en alarmhistorie intact.

Velden

  • userId + boatId String @unique
  • resetAt DateTime
  • updatedAt DateTime

Relaties

  • hoort bij precies 1 User en 1 Boat
fleetBoatDataUsageReset

Niet-destructieve maand- en categoriebaseline waarmee één boot vanaf nul kan worden weergegeven terwijl bronrapporten en budgethandhaving intact blijven.

Velden

  • boatId String @unique
  • periodStart + resetAt DateTime
  • uplink baselines BigInt
  • attribution generation + baselines String? + BigInt?

Relaties

  • hoort bij precies 1 Boat
telemetryMetricDefinition

De herbruikbare catalogus van grootheden. Hier — en nergens anders — staat of iets openbaar mag zijn en of het alarmen ondersteunt.

Velden

  • code String @id— temperature, humidity, location, …
  • valueType Enum— number | boolean | location
  • canonicalUnit String?
  • minimumValue / maximumValue Decimal?— fysieke grens
  • isPublicAllowed Boolean— location = false
  • supportsAlerts Boolean

Relaties

  • definieert 0..n Sensor
telemetrySensor

De stabiele logische identiteit die de boot stuurt. Boat + externalId is uniek, en de metric mag na registratie niet meer veranderen.

Velden

  • externalId String— bijv. cabin.temperature
  • name String— mag wél wijzigen
  • lastSeenAt DateTime

Relaties

  • hoort bij 1 MetricDefinition
  • levert 0..n SensorReading
telemetryTelemetryObservation

Eén atomaire momentopname met maximaal één reading per sensor. Boat + observationId is uniek; dát maakt retries veilig.

Velden

  • observationId String— idempotentiesleutel
  • observedAt DateTime— tijd op de boot
  • receivedAt DateTime— tijd in de cloud
  • payloadHash String— volgorde-onafhankelijk; verschil = 409

Relaties

  • bevat 1..n SensorReading
telemetrySensorReading

Precies één waardevariant per reading: een getal, een boolean, of een gekoppelde LocationValue. Applicatievalidatie én databasechecks dwingen dat af.

Velden

  • numberValue Decimal?
  • booleanValue Boolean?

Relaties

  • heeft 0..1 LocationValue
telemetryLocationValue

Een coördinaat is bewust één waarde en geen twee losse sensoren, zodat je nooit een breedtegraad van 10:00 met een lengtegraad van 10:05 combineert.

Velden

  • latitude Decimal— -90 … 90
  • longitude Decimal— -180 … 180
  • accuracyMeters Decimal?— 0 … 100.000

Relaties

  • hoort bij precies 1 SensorReading
alertingAlert

Eén duurzaam incident per doorlopende regelconditie, met gekozen ernst en een aparte actief-, afhandel- en hersteltoestand.

Velden

  • type String— FROST | HUMIDITY | BATTERY | BILGE | SHORE_POWER | CONDENSATION | GEOFENCE | OFFLINE | SENSOR_OFFLINE | FORECAST_WIND | PRESSURE_DROP
  • severity String?— INFORMATIEF | WAARSCHUWING | KRITIEK
  • lifecycleVersion Int— 0 = historie; 1 = incident
  • active / startedAt / resolvedAt Boolean / DateTime?
  • stateKey / activeIncidentKey String?— actieve sleutel is uniek
  • acknowledged / acknowledgedAt Boolean / DateTime?— staat los van herstel
  • deletedAt DateTime?— geaudite soft-delete van inactieve historie

Relaties

  • hoort bij 1 Boat en 0..1 Sensor
  • heeft 0..n NotificationDelivery
alertingBoatAlertSetting

Alleen de afwijkingen van de standaard worden opgeslagen. Een ontbrekende rij betekent de veilige default — daardoor was er nooit een backfill nodig.

Velden

  • category String
  • enabled Boolean
  • threshold Decimal?
  • delayMinutes / cooldownMinutes Int
  • recoveryEnabled Boolean

Relaties

  • hoort bij 1 Boat
alertingAlertRuleState

De serverside toestandsmachine voor vertraging, hysterese, cooldown en herstel. Bewaart alleen tijdstippen en id's, nooit gekopieerde meetwaarden.

Velden

  • stateKey String @unique— boot + categorie + optionele sensor
  • active Boolean
  • conditionSince / lastAlertAt / lastRecoveryAt DateTime?

Relaties

  • hoort bij 1 Boat
alertingNotificationPreference

Expliciete toestemming per gebruiker, boot en kanaal, inclusief de versie van de toestemmingstekst en stille uren met tijdzone.

Velden

  • channel String— PUSH | EMAIL_FALLBACK
  • consentVersion / consentGrantedAt String? / DateTime?
  • quietHoursStartMinutes Int— 1320 = 22:00
  • timezone String— IANA, standaard Europe/Amsterdam

Relaties

  • uniek per user + boat + channel
alertingNotificationDelivery

De transactionele outbox. Bevat bewust geen berichttekst, meetwaarde, coördinaat of provider-endpoint — alleen een opake destinationKey.

Velden

  • status String— PENDING | DEFERRED | RETRYING | ACCEPTED | FAILED | SUPPRESSED
  • destinationKey String— opake PushSubscription-id
  • occurrenceKey / occurrenceType String— INITIAL | REMINDER | RECOVERY
  • attemptCount / nextAttemptAt Int / DateTime
  • leaseToken / leaseExpiresAt String? / DateTime?— tijdelijke claim voor één dispatcher
  • failureCode String?— privacy-veilige code

Relaties

  • uniek per alert + user + channel + destination + occurrence
controlControllableOutlet

De cloud-identiteit van één goedgekeurde stekker. Home Assistant-entity-id's staan uitsluitend in de root-owned Edge-configuratie.

Velden

  • externalId String
  • model String— alleen SONOFF_S60ZBTPF
  • adminEnabled Boolean
  • lastSeenAt DateTime?

Relaties

  • hoort bij 1 Boat
  • is doel van 0..n OutletCommand
controlOutletCommand

Een expliciete gewenste eindtoestand met korte levensduur. activeOutletKey is de serialisatiegrens op databaseniveau.

Velden

  • activeOutletKey String? @unique— gevuld zolang PENDING of DELIVERED; daarna leeg
  • desiredState Enum— ON | OFF — geen toggle
  • status Enum— PENDING | DELIVERED | SUCCEEDED | FAILED | EXPIRED
  • expiresAt DateTime— aanmaak + 30 s

Relaties

  • hoort bij 1 ControllableOutlet en 1 User
controlOutletCommandAudit · AdminAuditLog · AlertSettingAudit

Bewijsregels met losse id-velden in plaats van cascaderende relaties, zodat de historie leesbaar blijft nadat een account, boot of stekker is verwijderd.

Velden

  • actorUserId / boatId / targetId String?
  • action / eventType String
  • resultCode String?

Relaties

  • nooit tokens, wachtwoordhashes, telemetriewaarden of HA-entities
commerceOrder

Kan zonder account geplaatst worden. De bedanktpagina zoekt uitsluitend op publicToken, nooit op een intern id.

Velden

  • amountCents Int— altijd hele eurocenten
  • status String— PENDING | PAID
  • publicToken String @unique— crypto.randomBytes
  • userId String?— gastbestellingen koppelen is handmatig

Relaties

  • hoort optioneel bij 1 User

Stabiele sensor-id's

Een Home Assistant-entity mag hernoemd of vervangen worden zonder dat de Bootwacht-identiteit verandert. Verandert de fysieke betekenis wél, dan introduceer je een nieuw logisch id in plaats van het oude te hergebruiken.

Idempotentie is een datamodelbeslissing

Niet 'de client stuurt hopelijk niet dubbel', maar een unieke constraint op boat + observationId en op alert + user + channel + destination + occurrence. Een retry kán dus geen dubbele meting of dubbele melding maken.

Geen brede tabel met een kolom per sensorsoort

Genormaliseerde observaties en readings betekenen dat een nieuwe sensor geen migratie vraagt. Een onbekende numerieke of booleaanse metric wordt bij eerste levering automatisch geregistreerd — privé en zonder alarmen, tot iemand hem bewust promoveert.

Dezelfde structuur aan boord en in de cloud

De Pi kent geen Boat (hij ís er één) en heeft extra leveringsvelden, maar verder is het model identiek. Dat maakt het redeneren over 'waar staat deze meting nu' eenvoudig.

Contracten en limieten

De harde getallen

Deze waarden staan in de contractdocumenten en worden op precies één plek gecontroleerd. Verandert er één, dan verandert het contract mee.

Ingebouwde metrics, eenheden en grenzen

Een waarde buiten deze grenzen levert HTTP 400, niet 500 — een kapotte sensor mag geen serverfout heten. Batterijpercentage en accuspanning zijn expres losse metrics.

MetricTypeEenheidGeaccepteerd bereik
temperaturegetalCel-50 … 80
humiditygetal%0 … 100
battery_percentgetal%0 … 100
battery_voltagegetalV0 … 60
water_detectedboolean—true / false
shore_powerboolean—true / false
contact_openboolean—true / false
locationlocatie—lat -90…90, lon -180…180

Nieuwe scalaire metrics worden geaccepteerd bij een geldige kleine-letternaam, een eindige waarde en een eenheid van hoogstens 16 tekens. Ze krijgen automatisch een privé, niet-alarmerende definitie. Eigen locatie-objecten worden niet geaccepteerd.

Telemetriecontract v1

Alarmcategorieën en veilige standaardwaarden

De eerste vijf regels reproduceren precies het gedrag van vóór de instellingenpagina, zodat bestaande boten niets merkten van de invoering.

CategorieStandaardErnstKlantbereikCooldownHysterese
Lage temperatuuraan, 3 °Cwaarschuwing-5 … 10 °C60 min0,5 °C
Hoge luchtvochtigheidaan, 85 %waarschuwing60 … 95 %60 min5 procentpunt
Lage accuspanningaan, 12,0 Vkritiek10,5 … 13,0 V60 min0,3 V
Water in de bilgeaankritiekvaste richting60 minconditie moet opheffen
Verlies walstroomaankritiekvaste richting60 minconditie moet opheffen
Dauwpuntmargeuit, geen grenswaarschuwing0,5 … 20,0 °C60 min0,5 °C
Geen nieuwe metingenuit, 120 minwaarschuwing30 … 1.440 min360 minnieuwe ontvangst

Ernst verlagen, een regel uitzetten, een drempel minder gevoelig maken, vertraging of cooldown verhogen, of herstel uitzetten vraagt een expliciete tweede bevestiging in UI én API. Bewegings- en geofence-instellingen zijn nog niet beschikbaar: een positiepunt ontvangen levert op zichzelf nog geen goedgekeurde bewegingsregel.

Alarminstellingen-contract

Grenzen en limieten die je moet kennen

OnderwerpWaardeWaarom
Telemetriepayloadmax. 64 readings, 64 KiBbegrensde parsekosten per request
Ratelimit live telemetrie120 / uur / bootgereserveerd voor actuele status
Ratelimit backfill720 / uur / bootbegrensd herstel zonder live data te blokkeren
Ratelimit inloggen10 / 5 min / IPtegen wachtwoordgokken
Ratelimit registreren5 / 10 min / IPbeperkt accountenumeratie
Ratelimit checkout10 / 10 min / IPtegen orderspam
Geldigheid schakelcommando30 svoorkomt uitvoering na een storing
Commandobody16 KiBapparaat- én klantendpoints
Verouderde energiedata> 2 uurdekt twee gemiste snapshots
Offline-detectie vloot> 2 uur geen metingrode markering in beheer
Retentie cloud / boot90 / 3.650 dagenAVG-minimalisatie vs. data-eigendom
Sessieduur30 dagenintrekbaar via tokenVersion
Pilotplekken25regio IJsselmeer en Friesland

Vertrouwensgrenzen

Beveiliging is structuur, geen afwerking

Deze maatregelen bepalen de vorm van de architectuur en zijn geen optioneel laatste laagje.

Apparaat → cloud

Bootspecifiek device-token

Telemetrie wordt alleen geaccepteerd voor de boot die bij het token in de header hoort. Het token komt uit crypto.randomBytes, wordt bij aanmaken één keer getoond en kan in het beheer geroteerd worden — waarna het oude direct stopt.

Browser → cloud

HttpOnly-sessie met databasecontrole

De cookie is onleesbaar voor browserscripts en het JWT-algoritme staat vast op HS256. Elke controle kijkt óók of de gebruiker nog bestaat en of tokenVersion klopt, zodat sessies centraal ingetrokken kunnen worden.

Formulier → API

Origin-check naast SameSite

Inloggen, registreren, checkout en klantmutaties eisen een toegestane Origin. Requests zonder Origin — apparaten, webhooks, curl — blijven bewust werken, want die zijn geen browserbedreiging.

Publiek → privé

Eén geauditeerde publieke uitzondering

Alleen het nieuwste geldige punt van de ene boot met isPublic=true is publiek. Historie, bewegingsalerts, andere boten, klantgegevens en device-tokens blijven uitgesloten — en de publieke pagina raakt de notificatiemodellen niet aan.

Configuratie → runtime

Fail-closed in productie

Zonder AUTH_SECRET start productie niet, zonder Stripe-sleutel én zonder expliciete DEMO_MODE=1 staat bestellen uit, en een startscript valideert de productieomgeving. Ontbrekende configuratie mag nooit stilletjes een onveilig pad openen.

Beheerder → boot

Geauditeerde bevoorrechte inzage

Een beheerder mag elk bootdashboard openen, maar elke inzage schrijft een AdminAuditLog. Beheerweergaven zijn read-only voor telemetrie, commando's en audit, en tonen nooit tokens, wachtwoordhashes of Push-endpoints.

Software → fysieke wereld

Schakelen is een gemak, geen veiligheidsfunctie

Alleen een goedgekeurd model, alleen een lokaal gemapte entity, alleen expliciete eindtoestanden, met een verlooptijd van 30 seconden en een geverifieerd opstartgedrag 'Off'. Walstroom, bilgepompen, verwarming en machines vallen buiten scope.

Netwerk → browser

CSP en beveiligingsheaders

frame-ancestors none, nosniff, een strikt referrer-beleid, permissions-policy zonder camera, microfoon en geolocatie, HSTS in productie, en frame-src die uitsluitend OpenStreetMap toelaat.

Bewust geaccepteerd

Restrisico's die opgeschreven staan

Registratie meldt dat een e-mailadres al bestaat (enumeratie, gemitigeerd door de ratelimit). De ratelimiter zit in procesgeheugen en vraagt bij meerdere instanties een gedeelde store. En npm audit meldt twee moderate findings in de door Next.js gebundelde postcss waarvoor geen patch bestaat — 'npm audit fix --force' draaien is hier schadelijk.

Deploymentbeeld

Waar de software echt draait

WerkendRaspberry Pi 4 · 4 GB · SATA-SSD

Raspberry Pi aan boord van elke boot

  1. 1Home Assistant (container)Eigen database en apparaatintegraties
  2. 2Bootwacht Edge (container)UID 10001, geen capabilities, read-only rootfs
  3. 3Edge-SQLite op de SSDLange historie plus de leveringswachtrij
  4. 4Uitsluitend uitgaand HTTPSWerkt door mobiele NAT; geen inkomende poort
In aanbouwVPS Linux S · 2 vCPU · 2 GB · 90 GB NVMe

Eén STRATO VPS in de EU

  1. 1Caddy op 80/443TLS-terminatie en het echte client-IP
  2. 2Eén Next.js-proces op poort 3000Privé gebonden, bewaakt door systemd
  3. 3Vier persistente systemd-timersOfflinebewaking en notificatiedispatch draaien iedere minuut; modelwind iedere 15 minuten en de hostwatchdog iedere 5 minuten
  4. 4OpenTelemetry metricsOnbevoorrecht en begrensd; hostmetrics plus twee attribuutloze activiteitstellers, geen logs of traces
  5. 5SQLite in /var/lib/bootwachtBuiten /opt/bootwacht — een release mag de database nooit vervangen
  6. 6Gesplitste secrets onder /etc/bootwachtRoot-beheerd, minste rechten, buiten Git en nooit in een log

Architectuurbesluiten

Vijf keer een bewuste afweging

Elk besluit staat als ADR in de repository, inclusief de afgewezen alternatieven en de voorwaarden om het te heroverwegen.

ADR-0001Zigbee als draadloos sensornetwerkGeaccepteerd · 2026-07-19
Aanleiding
De pilot moet snel op één boot werken met betaalbare, verkrijgbare sensoren. Zigbee, LoRaWAN, Matter en 4G lossen verschillende delen van het probleem op en worden vaak door elkaar gehaald.
Besluit
Zigbee voor het lokale sensornetwerk, met de Bootwacht-hub zelf als coördinator die de essentiële alarmlogica lokaal draait. Geen afhankelijkheid van Matter, een externe hub of het smart-home van de klant.
Wat het kost
2,4 GHz in staal geeft dode hoeken, dus antenneplaatsing hoort bij het installatieontwerp en soms is een gevoede repeater nodig. Er moet een expliciete compatibiliteitslijst komen: 'Zigbee' op de doos garandeert geen gelijk gedrag. LoRaWAN is uitgesteld naar een eventueel Bootwacht Marina.
Lees de ADR →
ADR-0002Eén STRATO VPS voor de pilotGeaccepteerd · 2026-07-21
Aanleiding
De applicatie gebruikt SQLite en een ratelimiter in procesgeheugen. Een stateless platform zou eerst een gedeelde database en gedeelde ratelimiter vereisen.
Besluit
Eén VPS Linux S in de EU met Ubuntu 24.04, Node.js 22, een eigen servicegebruiker, systemd en Caddy. Data buiten de releasemap, secrets buiten Git, precies één instantie.
Wat het kost
Je bent zelf systeembeheerder: updates, firewall, schijfruimte, logs en herstel. De VPS-schijf is persistent maar blijft één failure domain, dus een geteste back-up buiten de VPS is een harde voorwaarde vóór livegang.
Lees de ADR →
ADR-0003Alles uitgaand: telemetrie én commando'sGeaccepteerd · 2026-07-24
Aanleiding
Telemetrie en schakelopdrachten hebben tegengestelde logische richtingen, maar de boot zit achter carrier-grade NAT met een wisselend adres.
Besluit
De Pi begint beide verbindingen. Telemetrie wordt geüpload, commando's worden opgehaald via HTTPS-polling. Elk commando heeft een korte verlooptijd, expliciete statussen en een acknowledgement met de werkelijk waargenomen toestand.
Wat het kost
Polling kost regelmatige requests, ook als er niets te doen is, en geeft een begrensde vertraging. MQTT of een persistente WebSocket kan dat later vervangen: dat verandert het transport, niet de levenscyclus van een commando.
Lees de ADR →
ADR-0004Eén publiek locatiepunt voor IdefixGeaccepteerd · 2026-07-31
Aanleiding
Locatie is standaard privé, maar de publieke demopagina kon daardoor niet laten zien waar de boot ligt — en dat is nou juist wat het product verkoopt.
Besluit
Eén nauw begrensde uitzondering: het nieuwste geldige punt van uitsluitend de boot met isPublic=true, met meettijd erbij en een expliciet als vast benoemde terugvallocatie als er geen positie is.
Wat het kost
De publieke HTML en het OpenStreetMap-verzoek bevatten noodzakelijkerwijs die coördinaat, en de browser van de bezoeker maakt contact met OpenStreetMap. Elke uitbreiding — een track, meerdere publieke boten, een losse publicatieknop — vraagt een nieuw besluit.
Lees de ADR →
ADR-0005Transactionele notificatie-outboxGeaccepteerd · 2026-08-02
Aanleiding
Een pushprovider aanroepen binnen de telemetrietransactie zou sensor-ingestie koppelen aan een externe dienst, en een wachtrij in geheugen verliest werk bij elke herstart.
Besluit
Een provider-onafhankelijke outbox in dezelfde database. Alarm en bezorgregels ontstaan in één transactie, telemetrie belt nooit een provider, en vlak vóór verzenden wordt opnieuw geautoriseerd tegen toestemming, eigendom en bevestiging.
Wat het kost
SQLite volstaat voor één proces, maar dispatch over meerdere instanties vraagt een gedeelde database en coördinatie. En 'geaccepteerd door de provider' blijft applicatiestatus: het garandeert geen weergave of menselijke aandacht.
Lees de ADR →
ADR-0008KNMI HARMONIE voor modelwindGeaccepteerd · 2026-08-15
Aanleiding
Zonder windsensor wil de klant toch een actuele indicatie en tijdig een waarschuwing voor voorspelde harde wind bij de laatst bekende bootlocatie.
Besluit
Gebruik het gratis KNMI HARMONIE-raster server-side, decodeer afgeronde locaties lokaal met ecCodes en vermeld de bron. Gebruik bij oude/ontbrekende GPS een expliciet bevestigde reserveligplaats (ADR-0012). Toon windsnelheid en windstoten afzonderlijk, met historie doorgetrokken en toekomst gestippeld; evalueer een uitgeschakelde-standaard voorspellingsregel via systemd.
Wat het kost
De waarde is modelwind op 10 meter en geen sensor- of navigatiemeting. De gratis dienst heeft geen SLA en vereist GRIB-decodering; last-good data stopt daarom na 12 uur.
Lees de ADR →
ADR-0009Privacybegrensde servermonitoringGeaccepteerd · 2026-08-16
Aanleiding
De enkele pilot-VPS vraagt externe beschikbaarheidsbewaking en vroegtijdige waarschuwing zonder klant-, boot-, betaal- of omgevingsdata naar een provider te sturen.
Besluit
Gebruik Better Stack Free voor twee uptimechecks, een vijfminutenheartbeat, hostmetrics en twee attribuutloze bootactiviteitstellers via OpenTelemetry op de centrale server. De boxen houden alleen contact met de bestaande Bootwacht-API's. Gebruik daarnaast Sentry-compatibele serverfouten met een strikte laatste allowlist.
Wat het kost
Push en native certificaatwaarschuwingen vragen een betaald plan; e-mail plus de lokale TLS-watchdog vormen daarom de pilotbasis. Logs, traces en browsertracking blijven uit.
Lees de ADR →
ADR-0010Dataverbruik en fail-safe maandbudgetGeaccepteerd · 2026-08-15
Aanleiding
De pilot heeft meetgegevens nodig om latere klantdatabundels te kiezen en vermijdbaar mobiel verkeer te beperken.
Besluit
Tel kernelbytes op de actieve route per uplink en Amsterdamse maand. Houd monitoring en schakelen actief bij de cap, maar stel backfill en bulkwerk uit.
Wat het kost
Dit bewaart de privacy en werkt provider-onafhankelijk, maar blijft een schatting die handmatig met de provider wordt vergeleken.
Lees de ADR →

Roadmap

Wat af is, wat loopt en wat bewust niet gebeurt

  1. Gebouwd

    Genormaliseerde telemetrie v1

    Metricdefinities, stabiele sensoren, idempotente observaties, getypeerde readings en atomaire locatie — aan boord én in de cloud, met een simulator om het te oefenen.

  2. Gebouwd

    Instelbare alarmen per boot

    Zes categorieën met vertraging, hysterese, cooldown, instelbare ernst en optioneel herstel, plus alarmlijnen in de grafieken en een tweede bevestiging bij risicovolle versoepelingen.

  3. Gebouwd

    Schakelen op afstand met auditspoor

    Eén goedgekeurd stekkermodel, expliciete eindtoestanden, 30 seconden geldigheid, serialisatie op databaseniveau en verbruiksmeting met een eerlijk meetmoment.

  4. Gebouwd

    Web Push-apparaten en bezorging

    #77#78#113

    Toestemming, meerdere apparaten per boot, zelfbeheerde VAPID-sleutels en een service worker met begrensde startschermdetails zijn gebouwd. De persistente dispatcher past urgentie en stille uren toe, gebruikt herstelbare leases en begrensde retries.

  5. In aanbouw

    Productiedeploy en back-ups

    #7#34

    De basisserver is ingericht: updates, swap, key-only beheer, aparte servicegebruiker, UFW, Node.js en Caddy. Nog open: applicatiedeploy, secrets, DNS, TLS en — als harde voorwaarde — een geteste back-up buiten de VPS.

  6. Deels gebouwd

    Geverifieerde e-mail en e-mailfallback

    #2#79

    Accountverificatie en de expliciet ingeschakelde e-mailfallback voor kritieke alarmen zijn gebouwd. Orderbevestiging, gastorderclaim en wachtwoordreset blijven apart gepland in #2.

  7. Gepland

    Bewegings- en geofence-alarmen

    Positie wordt al opgeslagen, maar een positiepunt ontvangen is nog geen goedgekeurde bewegingsregel. Er moet een regel komen die onderscheid maakt tussen zwaaien aan de lijn en werkelijk vertrekken.

  8. Gepland

    Bootwacht Connect als echt abonnement

    Nu verkocht als eenmalig jaarbedrag. Echte Stripe Subscriptions zijn een beperkte wijziging: mode 'subscription' plus een Price-object.

  9. Verkenning

    Bootwacht Marina met LoRaWAN

    Veel boten op één stallingslocatie die een door de haven beheerde gateway delen. Dat is geen firmwareschakelaar: het vraagt andere hardware, credentials per apparaat, een netwerkserver, multi-tenant autorisatie en dekkingsplanning — plus gatewayredundantie, want anders is één gateway een gedeeld faalpunt.

  10. Bewust niet

    Certificering, meldkamer en verzekeraarsroute

    Geen SCM/VbV-certificering, geen 24/7-meldkamer en geen verzekeraarskorting-route: te professioneel en regulatoir voor deze fase. Ook sms, WhatsApp, Signal en bellen vallen buiten scope. De FAQ op de site zegt dit expliciet — liever eerlijk dan suggestief.

Begrippenlijst

De woorden die je nodig hebt

Bootwacht Edge
De Python-service op de Raspberry Pi die Home Assistant uitleest, observaties normaliseert en lokaal opslaat, ze naar de cloud levert en uitgaand op schakelopdrachten pollt.Zie ook: Observatie, Device-token
Observatie
Eén atomaire momentopname van alle gemapte sensoren op één tijdstip, met één observationId. Boat + observationId is uniek, dus opnieuw sturen is veilig.Zie ook: Idempotentie, Reading
Reading
De waarde van precies één sensor binnen een observatie. Een reading heeft exact één waardevariant: getal, boolean of gekoppelde locatie.
Idempotentie
Dezelfde opdracht twee keer uitvoeren geeft hetzelfde resultaat als één keer. Bij Bootwacht afgedwongen met unieke sleutels in de database, niet met hoop.
Device-token
Het geheim in de header x-device-token waarmee één specifieke boot telemetrie mag posten. Gegenereerd met crypto.randomBytes, eenmalig getoond, roteerbaar in het beheer.
MetricDefinition
De catalogusregel van een grootheid: type, canonieke eenheid, fysieke grenzen, of het openbaar mag zijn en of het alarmen ondersteunt.
Hysterese
De marge die voorkomt dat een alarm rond de drempel blijft aan- en uitflikkeren. Bij temperatuur 0,5 °C: onder 3 °C alarmeren, maar pas boven 3,5 °C herstellen.Zie ook: Cooldown
Cooldown
De minimale tijd voordat dezelfde actieve conditie opnieuw een herinnering mag opleveren. Standaard 60 minuten, 360 minuten voor de offline-regel en 1.440 minuten voor sensorstilte.
Transactionele outbox
Patroon waarbij een gebeurtenis en de bijbehorende bezorgopdracht in één databasetransactie ontstaan. Zo kan er geen alarm zonder melding bestaan en geen melding zonder alarm.
VAPID
Voluntary Application Server Identification: het sleutelpaar waarmee een server zich bij een Web Push-dienst identificeert. Zelf beheerd, dus zonder tussenpartij of abonnement.
Fail closed
Bij ontbrekende of kapotte configuratie kiest het systeem de veilige weigering in plaats van stilletjes doorgaan. Zonder AUTH_SECRET start productie niet; zonder betaalsleutels staat bestellen uit.
Carrier-grade NAT
De provider deelt één publiek IP-adres met veel klanten, waardoor de boot van buitenaf niet te bereiken is. Precies daarom initieert de Pi élke verbinding zelf.
Zigbee
Draadloos 2,4 GHz-netwerk met laag verbruik voor sensoren. Batterijsensoren slapen tussen berichten door; gevoede apparaten kunnen als repeater dienen.
Idefix
De Jeanneau Fantasia van de maker en de eerste bewaakte boot. Ook de naam van de publieke demopagina /idefix, bereikbaar via een QR-sticker aan boord.
Walstroom
De 230 V-aansluiting vanaf de kade. Bootwacht meet of die er is (verlies is een kritiek alarm), maar schakelt hem bewust nooit.
Bilge
Het laagste punt binnen in de romp, waar lekwater samenkomt. Water daar is een van de kritieke alarmen.

Veelgestelde vragen

De vragen die echt gesteld worden

Waarom is dit open source als het ook een product moet worden?

Omdat het verschil met een merkdoos juist de openheid is. Een eigenaar kan zien wat er gebeurt met zijn data, zelf een sensor bijprikken en het kastje blijven gebruiken als Bootwacht ophoudt te bestaan. De waarde zit in de gebouwde en ondersteunde combinatie, de installatie aan boord en de service in de regio — niet in geheime software.

Wat gebeurt er als de internetverbinding wekenlang wegvalt?

Aan boord verandert er niets: sensoren blijven meten en elke observatie wordt naar de SSD geschreven. De wachtrij loopt op. Na herstel levert Edge de nieuwste status eerst en voert daarna begrensde oudste backfill af met de originele id's, waardoor de cloud geen dubbelen krijgt. Oude waarden veranderen geen actuele sensor-alerts. Wat je in die periode mist, is het remote inzicht — daar is de offline-alarmregel voor.

SQLite in productie — is dat niet vragen om problemen?

Voor deze vorm niet. Eén proces, één schrijver, een handvol boten die elke vijf minuten een kleine payload sturen: dat is ver onder wat SQLite aankan. De echte beperking is niet de belasting maar de topologie — je kunt geen tweede instantie starten. Die grens staat expliciet opgeschreven, samen met wat er dan tegelijk moet veranderen.

Is dit een alarmsysteem?

Nee, en dat staat er bewust overal bij. Bootwacht is een informatiesysteem: het vertelt je wat er aan boord gebeurt. Er is geen meldkamer, geen certificering en geen garantie dat een melding op tijd gezien wordt. Het vervangt geen verzekering, gecertificeerd alarmsysteem of goed onderhoud.

Wie kan de positie van mijn boot zien?

Jij, en beheerders — waarbij elke beheerdersinzage een auditregel schrijft. Locatie is op metricniveau als niet-publiek gemarkeerd, dus geen enkele publieke query kan erbij. De enige uitzondering is de demoboot Idefix, waarvoor de eigenaar zelf toestemming gaf, en die uitzondering geldt voor precies één punt zonder historie.

Kan ik zelf een sensor toevoegen?

Ja. Koppel het apparaat in Home Assistant, voeg de entity toe aan de Edge-mapping met een eigen stabiel logisch id, en de eerste levering registreert de metric automatisch. Nieuwe metrics beginnen privé en zonder alarmondersteuning tot iemand ze bewust opneemt in de catalogus.

Waarom kan ik niet alles op afstand schakelen?

Omdat het onverwacht in- of uitschakelen van walstroom, een bilgepomp, verwarming of machines brand, wateroverlast of schade kan geven. Er is één goedgekeurd stekkermodel, met een geverifieerd opstartgedrag 'Off' zodat een stroomstoring niets aanzet. Uitbreiden vraagt een belastingstest en expliciete goedkeuring, geen codewijziging.

Ik wil meebouwen — waar begin ik?

Lees AGENTS.md voor de werkwijze, dan bootwacht-website/CLAUDE.md voor de applicatie. Werk vanuit een GitHub-issue met milestone, prioriteit, gebied en omvang, op een eigen branch, met een pull request. Documentatie hoort bij dezelfde wijziging: is een contract, ADR of deze pagina achterhaald, dan is de wijziging niet klaar.

Verder lezen

Van plaatje naar broncode

Deze pagina legt de vorm uit; onderstaande bestanden zijn leidend voor de details.

Eerste sessie

Anderhalf uur om je thuis te voelen

  1. 15 minLees deze pagina van boven naar benedenJe hoeft nog niets te onthouden. Je zoekt de vorm, niet de details.
  2. 20 minZet de site lokaal aan met testdatanpm run setup en npm run dev in bootwacht-website. Log in als beheerder en als demoklant en kijk hoe verschillend die twee dashboards zijn.
  3. 30 minVolg één meting door de hele stackVan de Edge-uploader naar /api/telemetry, door de validatie en Prisma heen, naar de alarmevaluatie en uiteindelijk de sparkline in het dashboard.
  4. 20 minDraai de telemetriesimulator tegen een testbootpython scripts/telemetry_simulator.py met normal, location, duplicate en alarm. Zie hoe duplicate rustig 200 teruggeeft en hoe alarm een alert in het dashboard laat verschijnen.
  5. 15 minLees één ADR en zoek het gevolg in de codeADR-0003 is een goede: zoek daarna de poll-endpoint en de unieke activeOutletKey terug in het schema.
  6. 10 minNeem de projectinvarianten doorKlantteksten in het Nederlands, bedragen in eurocenten, geen eigen betaal-UI, fail-closed authenticatie, locatie nooit publiek. Die staan in AGENTS.md en zijn niet onderhandelbaar.

Deze pagina is gegenereerd uit één databron in de repository (src/lib/architecture-guide). Een onderdeel, route, besluit of begrip toevoegen is een datawijziging — de opmaak hoeft niet aangeraakt te worden.