NuestroNido is a web app that lets a household stop coordinating its life inside a WhatsApp group: chores, shopping lists, shared notes and progress, all in one place.
The problem
Every group sharing a house ends up on the same system: a chat. It works until three questions need an answer at once. Who bought the detergent, who takes the trash out on Thursday, and where the plumber's number was written down. The chat answers all three, but only if someone scrolls far enough.
The problem isn't a lack of tools. It's that household information lives in channels that don't talk to each other: a list on one person's phone, a voice note, a message nobody can find anymore. That scatter has a concrete cost. Things get bought twice, tasks sit unassigned without anyone noticing, and the division of labor feels unfair even when nobody can prove it.
NuestroNido starts from a simple idea: if the household is the unit of shared life, the household has to be the entity in the system. Tasks, lists, notes and invitations hang off it and get read from one place.
One honest caveat before going further: this is a personal project. The problem is framed as a design hypothesis built on real friction, not as the output of interviews or adoption metrics.
What it does
Someone creates a nest and shares it with an eight-character code or a link. From there the household has four surfaces.
Tasks with daily, weekly (specific days) or monthly recurrence, assigned to a member and given a due date. A scheduled job clears and resets them periodically.
Shopping list with a closed set of categories (food, pharmacy, pets, cleaning, misc, other), quantities and purchase status.
Notes, shared, with an author.
Progress: completing a task awards between 10 and 20 experience points to the assigned member, and the total defines their level. The point is to make effort visible and on the record, which is exactly what's missing when the division of labor gets argued from memory.
On top of that: native browser push notifications and a fully bilingual interface.
Architecture
The backend is split into four layers with dependencies pointing one way. The HTTP layer validates and serializes, services decide, repositories query, and models don't know the other three exist. The rule holding it together is boring but effective: a service can't import anything from fastapi, and a repository can't hold business rules.
The most important decision sits somewhere unglamorous: where COMMIT happens. Repositories never close a transaction. They call flush(), which sends changes to PostgreSQL inside the open transaction so constraints get validated and generated IDs come back, but leaves it alive. The thing that closes the loop is the get_db dependency, which wraps the whole request: commit on success, rollback on any exception.
This matters because creating a nest is two dependent inserts. First the row in families, then the creator's membership, which needs the freshly generated family_id. If each repository committed its own work, a failure on the second insert would leave a household with no members. With the transaction boundary at the request level, the operation is atomic without any service having to remember to roll back.
The price is that no service can commit on its own, and scheduled jobs have to open their own session scope because they run outside a request lifecycle. I'll take that trade any day over thinking about orphaned data again.
The data model
Ten tables, all with UUIDs generated in the application. families is the root and everything else hangs off it.
Three decisions worth explaining.
A user belongs to exactly one household. family_members.user_id is unique. That was a product constraint before a technical one: it simplifies the entire permissions model, because every request resolves the household from the user and there's no ambiguity. If the product ever needed multi-household support, it's a matter of dropping the UNIQUE and adding an active household to the session.
Cascade deletes live in the engine. Deleting a household takes members, tasks, lists, notes and invitations with it through ON DELETE CASCADE. There's no Python-level cascade and no N round-trips: one statement resolves the whole graph, atomically.
tasks.xp_reward is an audit field. The awarded XP is random, so it has to be stored to be taken back. When a task gets unmarked, the system subtracts exactly the value it added. Without that column, the gamification would be irreversible.
What runs on its own
Four APScheduler jobs keep the system tidy without intervention:
| Job | When | What it does |
|---|---|---|
| Task cleanup | every 2 days, 00:01 | deletes expired one-off tasks, resets recurring ones |
| List cleanup | Sundays, 03:00 | removes items already purchased |
| Task reminder | daily, 09:00 | push to anyone with pending tasks |
| Shopping reminder | Thursdays, 18:00 | push to anyone with unpurchased items |
On the read side, Redis absorbs fast UI refreshes: ten seconds of cache on tasks and lists, one hour on static catalogs. The biggest win is the levels table, queried on every completed task to calculate a level-up; with caching, one query per hour is enough.
Operating it produces three signals. Latency and throughput metrics in Prometheus, errors with request context in Sentry, and structured logs. The /health endpoint checks the database connection, so an orchestrator can tell a live process from a working application.
Three things that cost me
XP was doubling. The first design awarded points every time a task reached completed. A double click or a client retry added them twice. The fix was to look at the previous state and store the awarded XP on the row. Completing twice now adds nothing, and unmarking subtracts exactly what was added.
Two simultaneous first logins broke user provisioning. The prior SELECT protected nothing: there's always a window between the check and the insert. I moved idempotency into the engine and the problem disappeared.
The Python cascade was a time bomb. It worked until a query failed halfway through. Moving it to ON DELETE CASCADE didn't just make it faster; it made it correct by definition.
If I had to do it again, I'd change two things. tasks.week_days stores the days as "1,3,5" in a string, which forces day filtering in memory; a normalized recurrence table would solve it in SQL. And APScheduler jobs run once per instance, so with more than one backend deployed every reminder would fire twice. Today there's a single instance, but it's logged technical debt.
Closing
NuestroNido wasn't a CRUD exercise. It had a real domain model (households, roles, expiring invitations), rules with side effects that had to be reversible, and a transaction boundary that had to be chosen on purpose.
What I take from it isn't FastAPI or Next.js. It's having had to decide, for every rule, where it lives: which constraint belongs to the database, which validation to the schema, which logic to the service, and which guarantee to the request. That distribution is what lets you reason about an operation without rebuilding the whole system in your head.