Skip to main content

Lifehouse — System Architecture

Version: 0.3.0
Last updated: 2026-02-20
Status: Prototype / Active Development


Overview

Lifehouse is a self-hosted community organising platform built around two distinct sub-projects: Fieldwork and Workbench. Both share a central PostgreSQL database on a dedicated server (CT 604) but are otherwise fully independent — separate services, separate networks, separate LLMs.

The guiding principle throughout is: own the infrastructure, own the data, keep it simple enough to explain to anyone.


Sub-Projects

Fieldwork

Door-knocking data capture pipeline. Volunteers use Telegram voice notes on doorsteps. The system silently handles transcription and structured data extraction. From the volunteer's point of view: talk, send, done.

Purpose: Build a live, searchable picture of community interest, skills, resources, and concerns — without forms, apps, or friction on the doorstep.

Data captured per conversation:

  • Address (raw spoken → normalised)
  • Contact outcome (interested, not home, wants info, etc.)
  • Skills the person mentioned
  • Resources they have (van, space, tools, etc.)
  • Concerns and issues raised
  • Volunteer who captured it

Future use: Sortition (Phase 2) — stratified random selection of assembly participants from the consented population pool.


Workbench

LLM-based curation of spreadsheet and structured data. Separate pipeline, separate LLM, separate network. Not covered in detail here.


Infrastructure

Hosts

HostRoleAccess
VeraLocal Docker host. Runs all containerised services.Local / WireGuard 10.200.0.2
Helsinki 2Proxmox hypervisor. Contains all CTs.SSH helsinki265.108.159.249 (root)
CT 604 lifehouse-dbDedicated Postgres 15 server for all Lifehouse data.192.168.100.250:5432

Network Routing

Vera reaches CT 604 via WireGuard → Helsinki 2 → vmbr1 (Proxmox internal bridge).

Routing is symmetric — no NAT/MASQUERADE on this path.

Helsinki 2 iptables rules (persisted in /etc/iptables/rules.v4):

  • FORWARD: wg0 ↔ vmbr1 (192.168.100.0/24) — allows packets in both directions

CT 604 static route (persisted in /etc/network/interfaces):

  • 10.200.0.0/24 via 192.168.100.1 — return path for WireGuard clients

Why no MASQUERADE? MASQUERADE caused asymmetric routing: SYNs arrived at CT 604 from 192.168.100.1 (masqueraded) but SYN-ACKs returned directly to 10.200.0.2 via the static route, causing Vera's TCP stack to reject them. Removing MASQUERADE and adding the static route on CT 604 makes the full conversation symmetric: 10.200.0.2 ↔ 192.168.100.250 end-to-end.


Fieldwork Pipeline

Volunteer (Telegram voice note)

Telegram Bot [NOT YET BUILT]

ingestion-api (Vera, port 8015, forge-net + n8n_default)

stt-service (Vera, GPU, Whisper large-v3 float16)

fieldwork-extractor (CT 604, systemd service, Ollama llama3 on Vera)

lifehouse DB (CT 604, schema: fieldwork)

Services

ServiceImage / StackNetworksStatus
ingestion-apiCustom Python/FastAPIforge-net, n8n_default✅ Running
stt-serviceCustom Python/faster-whisperforge-net, n8n_default✅ Running
fieldwork-extractorPython/FastAPI, systemd on CT 604localhost (CT 604)✅ Running
Telegram botTBD❌ Not built

ingestion-api

  • Receives audio uploads (multipart/form-data)
  • Calls stt-service for transcription
  • Persists every job to fieldwork.transcription_jobs on CT 604
  • Endpoints: POST /audio (sync), POST /audio/async, GET /status/{job_id}, GET /health
  • DB degrades gracefully if CT 604 is unreachable

stt-service

  • faster-whisper large-v3, float16
  • GPU-enabled (NVIDIA runtime)
  • Lazy model loading on first request
  • Endpoint: POST /transcribe, GET /health

fieldwork-extractor

  • Runs as a systemd service directly on CT 604 (/root/fieldwork-extractor/, Python venv)
  • Polls fieldwork.transcription_jobs for completed jobs not yet extracted
  • Locks each job (error = 'extracting') before calling Ollama to prevent duplicate extraction on retry
  • Calls workbench-ollama on Vera (http://10.200.0.2:11434, model llama3:latest) with a structured extraction prompt
  • Writes results to fieldwork.doorstep_contacts, fieldwork.people, fieldwork.households, fieldwork.skills, fieldwork.resources, fieldwork.concerns
  • Clears the lock and sets doorstep_contact_id on success; sets a descriptive error on failure
  • Source: /home/southville-forge/fieldwork-extractor/main.py (Vera), deployed to /root/fieldwork-extractor/main.py (CT 604)
  • Ollama timeout: 300s (llama3 cold-load can take ~90s)

Database — CT 604 lifehouse

Host: 192.168.100.250:5432
Database: lifehouse
Schema: fieldwork

Tables

TableOwnerPurpose
transcription_jobsingestion-apiRaw STT pipeline tracking. One row per audio file.
doorstep_contactsfieldwork-extractorStructured extraction per doorstep conversation.
peoplefieldwork-extractorIndividuals. Identifiable fields nullable (GDPR).
householdsfieldwork-extractorPhysical addresses. Deduplication unit.
locationsfieldwork-extractorPostGIS geometry, postcode, ward.
demographicsfieldwork-extractorSortition stratification fields. Separate from identifiable data.
organisationsfieldwork-extractorGroups and orgs encountered in fieldwork.
skillsfieldwork-extractorLLM-extracted skills per person.
resourcesfieldwork-extractorLLM-extracted resources per person.
concernsfieldwork-extractorIssues raised. For agenda building and driver mapping.
sortition_poolViewConsented people + demographics. Ready for Phase 2 sortition engine.

DB Users

UserAccessNotes
fieldwork_apiSELECT, INSERT, UPDATE on all fieldwork.* tablesUsed by ingestion-api and fieldwork-extractor. Change default password before real data.
postgresSuperuserAdmin only. Not used by application services.

Schema file

/home/southville-forge/Southville-Forge-System/Sites/Life House/schema/fieldwork_schema.sql
/root/lifehouse/schema/fieldwork_schema.sql (Helsinki 2 copy)


Workbench Pipeline

Separate from Fieldwork. Uses classifier-llm (SGLang/Qwen2.5-3B) and classifier-api.
Do not share LLM services between Fieldwork and Workbench.

ServiceImageNetworksStatus
classifier-llmlmsysorg/sglangclassifier-net✅ Running
classifier-apiCustom Python/FastAPIclassifier-net✅ Running (stub)
workbench-ollamaollama/ollamaworkbench_default✅ Running
workbench-apiCustomworkbench_default✅ Running
workbench-postgrespostgresworkbench_default✅ Running

Phase 2 — Sortition (Future)

Deterministic sortition engine. Not AI-driven. Rules and random seed are fixed and auditable.

  • Load consented population from fieldwork.sortition_pool view
  • Apply eligibility filters (consent, cooldown windows)
  • Stratify by demographic bands (age, gender, area, housing tenure, etc.)
  • Random selection within strata using a recorded seed (Python/NumPy RNG)
  • All parameters and results persisted for audit

The LLM role in Phase 2 is supportive only: clean messy inputs, explain selections in plain language. It never selects people or adjusts weights.


What's Not Built Yet

ComponentPriorityNotes
fieldwork-extractor✅ DoneRunning on CT 604 as systemd service
Telegram botHighReceives voice notes, forwards to ingestion-api
Address normalisationMediumPostcode lookup / geocoding for locations table
Consent web formMediumSimple self-hosted form for Phase 2 readiness
Sortition engineLowPhase 2 — after population pool is populated
Map visualisationLowLeaflet + PostGIS — after data exists

Key Credentials (Vera)

⚠️ Change fieldwork_api DB password before capturing real data.

  • lifehouse DB: fieldwork_api / fw_api_changeme_2026192.168.100.250:5432/lifehouse
  • forge postgres: forge / forgepasspostgres:5432/forge_db
  • n8n: https://vera-n8n.southvilleforge.comadmin / admin-132a1e24cb42
  • HF_TOKEN: stored in /home/southville-forge/southville-stack/.env

Repository / Version Control

No Git repository set up yet. Not required until there is a deployment target (website, public API, etc.). All source lives on Vera at /home/southville-forge/.

When Git is added, suggested structure:

lifehouse/
├── ingestion/ # ingestion-api
├── stt/ # stt-service
├── fieldwork-extractor/
├── workbench/
├── schema/ # SQL migration files
└── docs/ # Architecture and runbooks