Backyard: Private Family Social Network
A personal project: a private, self-hosted social network for one extended family, where a grandparent gets in with one tapped link and no account. Pre-release.
2026
- A restore always revokes every link, session and invite it brings back, so an old archive cannot return a removed relative's access
- CI plants a known-bad fixture for each security scanner to prove it still fires, and a test fails if a named CI step disappears
- One audience module behind every surface: when release 0.1.0 let each side of a family see the other side's replies on a post addressed to both, one fix corrected the feed, email and media together, and the release was withdrawn before anyone was invited
- Five CI jobs on every change, including ruff, strict mypy and the test suite against a real Postgres, and a mobile end-to-end run with an accessibility sweep
Problem
Group chats interrupt everyone and bury anything older than a day, and relatives outside your own household drift out of view. Research before any code set the constraints: it found no active, polished, family-focused open-source social network; reciprocity is a hard requirement, since in-home field studies of a family-awareness system for older adults found that a one-way first version left the grandparent with no way to answer, and a two-way redesign worked better; a smartphone cannot be assumed; and two sides of a family have boundaries that one shared feed would break.
Approach
The feed is chronological and it ends: no ranking, streaks, like counts or read receipts. Each household is a pod and each side of the family a yard, and members on one side never see the other side's members or replies, even on a post addressed to both. A grandparent taps one link, reads in large type and reacts with one tap, with no account. Seven decision records, a threat model with ten system-wide requirements and a written permission matrix govern the build. Later releases came from walking a live instance at phone width as each kind of user, one changed path at a time, because a self-audit found a grandparent on the no-login path could not see a photograph while every v1 user story was marked passing. Still to come: a scripted walk of the whole product before the first invitation, then a rehearsed admin hand-over and an outside security review before v1.0.
Architecture
One Linux VM runs four containers under Docker Compose: Caddy at the edge, the Django web app, a worker on the same application image, and PostgreSQL. Postgres holds all family data and also carries the job queue through Procrastinate, so there is no Redis and no message broker; media and encrypted backups live on one app volume. Only Caddy publishes ports, the database shares no network with the edge, and the app connects as a role with no superuser rights and no rights to change the schema. One audience module answers who may see what for every surface (the feed, the Email Updates digest, the grandparent's page, media, push and contact export), and every denial is the same 404 as a page that does not exist. Each time a grandparent's link is opened, it is exchanged for a session cookie and a clean address; the link stays valid until it is regenerated, and one Regenerate action kills it and every credential derived from it. Photos are re-encoded with their location data stripped, and the worker transcodes video, fetches link previews, sends push notifications and Email Updates, and runs the nightly encrypted backup and a weekly health email.
More Results
- A seeded first-schema database is migrated to the latest schema on every CI run
- Sixteen tagged releases, 0.1.1 to 0.3.6, from August to October 2026
Lessons Learned
A passing test suite did not mean a working product: every v1 user story was marked passing while a grandparent on the no-login path could not see a photograph, so later releases came from walking a live instance as each kind of user, one changed path at a time. Keeping every read decision in one module meant the cross-family reply leak needed only one fix. And a required check can stay green after its scan is deleted, because checks are jobs, not steps; a test now fails if a named step disappears.
