Kindred
Your people: kin and the friends who feel like it.
Kindred is a first-party family CRM: one shared, timestamped graph of relatives, friends, neighbours and colleagues that the whole household reads and writes together. Everyone signs in with their own account and sees the same people, relationships and history; anything that varies over time (names, relationships, residences) carries an interval, so a time slider shows the record as it stood at any moment. The one deliberate exception to sharing is per-user private notes and gift ideas that nobody else, not even the family admin, can read. It ships as one Go binary with the web UI embedded, and the data store is sealed with a passphrase until the server unseals it.
Installation
Architecture
- Deployment: Single
kindreddeployment in thekindrednamespace, one replica,Recreatestrategy - Image:
gitea.web.kueber.eu/johnny/kindred, a static Go binary with the web UI embedded; the repo's own Gitea workflow builds and pushes it to the private registry, and thesecrets.docker-giteanamespace label delivers theregcred-docker-giteapull secret - Storage: Longhorn PVC (
data, 1Gi,longhorn-encrypted) at/data, the app's data directory - Database: SQLite on the data volume (
KINDRED_DB_PATH), encrypted at rest with theKINDRED_DB_KEYauto-unseal passphrase; no database component - Networking: ClusterIP service on port 8080, HTTPRoute via the public gateway on
kindred.web.kueber.eu - Priority:
tier-2(an outage is felt, nothing irreplaceable is lost), requests of 25m/64Mi with the memory limit at twice the request; CPU is requested but never limited
Security
- Runs as uid 10001,
runAsNonRoot: true, all capabilities dropped,RuntimeDefaultseccomp, no service account token; the root filesystem is not read-only - Ingress only from the public gateway's Envoy pods. On top of the app-network-policy baseline (DNS, the namespace, the gateway),
egress-kindredopens 443/80 to the internet so the optional background geocoder (KINDRED_GEOCODE_*) can reach a public geocoding endpoint - The unseal passphrase is delivered as
KINDRED_DB_KEYfrom the SOPS-encryptedkindred-configSecret, so the server auto-unseals at boot instead of waiting for a manual unseal on the web gate - The Longhorn PVC is encrypted at rest via a SOPS-managed key (
data-longhorn); first-party images from the Gitea registry fall under the audit-mode cosign policy described in image supply chain
Updates
The deployment pins latest@sha256:… and pulls with imagePullPolicy: Always; Renovate moves the digest whenever the Gitea workflow pushes a new build.
Data Management
- PVC:
data(1Gi, Longhorn-encrypted), annotatedk8up.io/backup: "true" - Backups: k8up
Schedules to Hetzner and Backblaze (workload-talos-kindred-*) via restic from the backups component: daily backup, weekly check and prune, retention of 5 last, 7 daily, 4 weekly, 12 monthly and 5 yearly snapshots - Provisioning: the workspace component creates the restic buckets and the DNS record for the hostname through a
TofuWorkspace - The backed-up data directory is only readable with the unseal passphrase, which lives in the SOPS-encrypted Secret, not in the backup
User Management
Accounts are Kindred's own; sign-in is in-app OIDC against Keycloak in multi-provider mode (KINDRED_OIDC_PROVIDERS with the keycloak provider's name, issuer, client id and secret, plus KINDRED_OIDC_REDIRECT_URL), all from the kindred-config Secret. The app holds the client secret and issues its own session, so a Keycloak outage breaks new logins but not established sessions, as described in identity end-to-end.
Configuration Management
- Everything the app reads comes from the SOPS-encrypted
kindred-configSecret, loaded as environment: OIDC provider settings, geocoder toggle and user agent, unseal passphrase - The Longhorn volume key, restic password and tofu state key in their own SOPS Secrets
Usage
Sign in through Keycloak, add people and the facts about them, link relationships and residences with the dates they held, and drag the time slider to see the graph at an earlier point. Private notes and gift ideas stay with the account that wrote them.
Cluster-specific deviations from the above live next to the manifests — see k8s/apps/kindred/talos; there is no per-cluster README yet.
Cluster Deployment
- Image:
gitea.web.kueber.eu/johnny/kindred:latest@sha256:d8d20b3cf378745e60d1b84bdf39189af8b8c6e2ef64b77fa1d0c11cf601262c
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app.kubernetes.io/name: kindred
name: kindred
namespace: kindred
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: kindred
strategy:
rollingUpdate: null
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: kindred
spec:
automountServiceAccountToken: false
containers:
- command:
- /kindred
envFrom:
- secretRef:
name: kindred-config
image: gitea.web.kueber.eu/johnny/kindred:latest@sha256:d8d20b3cf378745e60d1b84bdf39189af8b8c6e2ef64b77fa1d0c11cf601262c
imagePullPolicy: Always
name: kindred
ports:
- containerPort: 8080
name: web
protocol: TCP
readinessProbe:
failureThreshold: 1
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 2
resources:
limits:
memory: 128Mi
requests:
cpu: 25m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
volumeMounts:
- mountPath: /data
name: kindred-data
imagePullSecrets:
- name: regcred-docker-gitea
priorityClassName: tier-2
securityContext:
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
runAsGroup: 10001
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
volumes:
- name: kindred-data
persistentVolumeClaim:
claimName: data