Perri.Sync
App compartida del hogar para gastos, calendario, hábitos y tareas, con una capa de juego WebGL y una API .NET detrás.
- Resultado
- Un producto del hogar para gastos, calendario, hábitos y tareas, con API .NET y hogares multi-tenant con Auth0.
- Problema
- Las parejas corren la casa entre planillas y chats hasta que algo se pierde entre apps.
- Decisión
- Cuatro repos (dashboard, landing, API, juego WebGL) con JWT y scope por hogar, en vez de un monolito o contraseñas compartidas.
- Impacto
- La operatoria compartida de la casa vive en un solo producto, con una capa de juego para que las tareas se abran porque querés, no solo porque toca.
Métricas
Blast radius del tenant
1 hogar
Gate de auth
JWT + membership


- Stack
- .NET 10
- Next.js
- React
- TypeScript
- Auth0
- WebGL
- Tailwind CSS
- Inicio
Contexto
Gastos en una planilla. Tareas en otro chat. El calendario en otro lado. Así terminan corriendo la casa un montón de parejas, hasta que algo se pierde entre apps.
Armé Perri.Sync para que eso viva en un solo lugar: gastos, calendario, hábitos, tareas, comidas, un chat chico y cuidado de mascotas. La capa de juego hace que “¿quién lava los platos?” sea algo que abrís porque querés, no solo porque toca.
Restricciones
- Los repos quedan privados. Esta página tiene que mostrar el producto sin el árbol de código.
- Una persona puede estar en más de un hogar sin mezclar datos.
- Free vs premium se tiene que poder exigir en la API, no solo en una página de precios.
- La capa WebGL usa las mismas asignaciones que la UI checklist. Dos pieles, un modelo.
Decisión de arquitectura
Cuatro repos, un producto. El diagrama de arriba es el camino del usuario: landing → Auth0 →
dashboard (elegir hogar) → API, y el juego WebGL reusa el mismo JWT y X-Household-Id.
¿Por qué no un monolito? El build del juego, el sitio de marketing y la API salen en
relojes distintos. ¿Por qué no una contraseña compartida para la casa? Auth0 por usuario
más membership del hogar es el trust model.
| Pieza | Repo | Rol |
|---|---|---|
| Dashboard | Perri.Sync.Dashboard.New | App principal, todas las funciones |
| Landing | Perri.Sync.Landingpage | Marketing y registro |
| API | Perri.Sync.Api | Backend REST (Auth0 JWT, EF Core) |
| Game | Perri.Sync.Game.WebGL | Capa interactiva de tareas |
Landing es la cara pública: qué hace, los planes y cómo entrás.
Dashboard es Next.js 15, React 18, TypeScript, Tailwind, Auth0, TanStack Query,
Recharts. Tres temas con next-themes: Light, Dark y Game (UI tipo Sims alineada a la capa
WebGL). Mismo modelo de datos, mismas llamadas a la API, otra piel.
Game sincroniza asignaciones desde la API. Completás tareas dentro de una escena WebGL life-sim o volvés a la checklist. Mismo hogar, dos formas de entrar.
API es .NET 10 en capas controllers → services → repositories → models, con FluentValidation en los DTOs. Una muestra de la superficie (hay más dominios: calendar, habits, chat, meals, pets):
| Dominio | Endpoints | Feature |
|---|---|---|
| Expenses | GET /expenses/monthly, GET /expenses/summary |
Grilla de gastos compartidos |
| Chores | GET /chores/assignments, PUT /chores/assignments |
Tareas diarias → juego |
| Settings | GET /settings, POST /settings/members |
Hogares multi-miembro |
| Avatar | GET/PUT /avatar |
Perfil / personaje del juego |
GET /api/v1/expenses/monthly?year=2026
Authorization: Bearer {jwt}
X-Household-Id: {household-guid}
Seguridad / blast radius
JWT de Auth0 en cada ruta. X-Household-Id scopea el request. La membership se chequea en
el server para que un hogar no lea otro cambiando el header (IDOR clásico si te salteás ese
check). El diagrama de workflow de arriba es el camino del producto. El dashboard guarda el
hogar activo en localStorage y lo manda en cada call; cambiar de casa cambia el header,
no los claims del JWT. El tenant no va embebido en el token a propósito: la membership es
un join en DB entre Auth0 sub y household id. Fail closed en la capa de servicio:
Member? member = await _householdContextResolver.ResolveMemberAsync(
auth0Id,
householdId,
includeHousehold: false,
includeNotificationPrefs: false);
if (member == null)
return Array.Empty<ChoreDto>(); // fail closed
return await _choreRepo.GetByHouseholdIdAsync(member.HouseholdId, cancellationToken);
Los hogares free tienen chat y gastos. Premium desbloquea chores y más miembros. Ese gate
vive en la API. Swagger queda en desarrollo en /swagger solamente.
Blast radius de un JWT filtrado: un usuario en los hogares a los que pertenece, no todo el producto, mientras la autorización por hogar se mantenga honesta.
Ops
Los colaboradores abren los links de repos privados; esta página muestra los walkthroughs de arriba. Swagger para trabajo local de API. Temas y Auth0 forman parte del deploy de la app, no de un producto de consola aparte.