Vecinos

In production · pre-1.0 · no external security review yet

One colonia. One box. The rules live in the database.

Vecinos is a neighborhood-association platform for a single colonia in Mexico: a Reddit-style feed with real anonymous posting, transparent append-only finances, and an idea → discussion → vote → action pipeline. It is built by one person and runs on one cheap machine.

The shape

A modular monolith, on purpose.

One deployable, one module per part of the domain, and vertical slices inside each. There is no distribution problem here worth buying: the whole product fits on one small machine, so splitting it would trade a design problem for a network problem.

One deployable · .NET
  • Identity 2 public methods
  • Media 2 public methods
  • Search 2 public methods
  • Feed 2 public methods
  • Neighborhood 2 public methods
  • Finance 2 public methods
  • Governance 2 public methods
  • Registry 2 public methods
  • Commons 2 public methods
  • Notifications 2 public methods
  • Platform 2 public methods
Shared kernel — Result, Money, Actor, IClock
  • Narrow seams. Each module is internal by default and exposes exactly two entry points. Modules never reach into each other; they share small contract records and events. An architecture test fails the build if one does.
  • Errors are values. Expected failures are a Result, not an exception, and one place maps them to HTTP status.
  • Immutable domain. A state change is a pure function returning the new state and the events it caused.

Three things worth a look

The parts that make it more than CRUD.

A neighborhood app is only useful if people trust it with their money and their voices. Each of these is enforced where it is hardest to bypass.

01

The ledger is append-only, enforced by a missing GRANT

The app's database role has no UPDATE or DELETE on the ledger table. A bug, a bad migration or a curious admin cannot rewrite history, because Postgres refuses. Mistakes are fixed with a reversing entry that carries a mandatory reason, and a balance is never stored: it is a fold over the entries.

✓ SELECT ✓ INSERT ✕ UPDATE ✕ DELETE

A test attempts the UPDATE as the app role and asserts the database refuses.

02

Anonymity is a projection, with one boundary

The author is always stored, so moderation and accountability stay possible. Anonymity is decided in exactly one place — PostView.From(post, viewer) — which every read goes through. Admins get a pseudonym and an unmask action that is audited, and the person who posted can see that it happened. Counters filter anonymous posts too, so a per-house number cannot give one away.

  • The author Their own post, marked as theirs
  • A neighbour Anonymous
  • An admin A pseudonym and an audited “unmask” button, never a name

03

One vote per house is a domain rule

An upvote on an idea is casual and per person. A formal poll is binding and per house, however many people live in it. Treating the two as one thing would make a governance result meaningless, so they are separate concepts with separate tests.

What it is made of

Boring where it can be, careful where it counts.

Mainstream, well-understood tools, chosen so one person can run the whole thing and a stranger can read it. The interesting decisions are in how they are used, not in which ones they are.

Backend

One .NET process, one module per part of the domain.

  • ASP.NET Core
  • C# records
  • EF Core
  • OpenTelemetry
  • Web Push

Frontend

A phone-first PWA, judged against Instagram and Mercado Pago.

  • Angular
  • TypeScript
  • Design tokens
  • Storybook
  • Astro (these sites)

Data

Each module owns its schema; the database enforces the invariants.

  • PostgreSQL
  • Elasticsearch
  • Valkey
  • SeaweedFS
  • Cloudflare R2
  • Mailpit

Infrastructure

Each app in its own rootless Docker, all on one machine.

  • Docker Compose
  • Rootless Docker
  • Caddy
  • Cloudflare Tunnel
  • Ansible

Observability

Logs and metrics in one place, with alerts that reach a person.

  • Grafana
  • Prometheus
  • Loki
  • Alertmanager
  • Uptime Kuma

Tests and tooling

Behaviour at the module boundary, against real services, one command each.

  • xUnit
  • Testcontainers
  • Vitest
  • Playwright
  • axe-core
  • Architecture tests
  • just
  • Forgejo Actions
  • Valkey

    An optional second-level cache. Without a connection string the app simply does not use one, so local runs, tests and CI need nothing extra.

  • SeaweedFS → R2

    Uploads go straight from the phone to object storage with a short-lived, single-object signed URL. SeaweedFS stands in for it on a laptop; in production it is Cloudflare R2, which has no egress fees and gives photos an offsite copy for free.

  • Elasticsearch

    Search is a projection, fed by events. The feed publishes “a post was created” and never learns that a search index exists.

  • OpenAPI

    The API’s contract is a committed file, and a test fails the build if the running API disagrees with it. The frontend’s types are generated from that file, so they cannot drift by accident.

  • No traces

    Every call between modules is an in-process method call, so a distributed trace would show almost nothing for the cost of running one. Logs and metrics answer “what is it doing, and how fast”.

From a fresh clone to a running system

$ just doctor # what is missing, and exactly how to fix it
$ just up     # infrastructure in Docker; API and Angular run natively
$ just reset  # drop → migrate → seed a synthetic colonia

If something needs a README paragraph instead of a recipe, that is a smell in the tooling, so the recipes are the documentation.

The box

A used desktop under a desk.

An i7-4770 with 32 GB of RAM, running production, staging, the CI and this page. Every app is its own Linux user with its own rootless Docker, so one compromised service does not get the others.

Colophon

Built by one person; the Spanish guides for residents and the board were written by another. The design leans on three people whose work is structural here rather than decorative: Kevlin Henney for simplicity and narrow seams, Venkat Subramanian for pure transitions and balances as folds, and Nickolas Means for blast-radius containment and human trust.

Maturity, plainly: in production for one colonia, pre-1.0, and no external security review yet. APIs may still change.