Gastos compartidos entre amigos, autoalojado.
Como hacer una vaquita, pero sin que nadie tenga que llevar la cuenta en una servilleta.
Armás el grupo para el viaje, empezás a cargar gastos, y cuando por fin se suman los que faltaban resulta que sólo cuentan desde ahí en adelante: todo lo anterior quedó dividido entre los que estaban. Para arreglarlo hay que editar gasto por gasto a mano, o dejar las cuentas mal.
Vaquita nació de esa bronca. Podés recalcular los gastos ya cargados para incluir a quien llegó tarde, viendo cómo quedan los saldos antes y después de aplicarlo.
Lo demás sale de autoalojarlo: sin publicidad, sin cuenta premium, sin límite de gastos por mes, y con quién le debe cuánto a quién guardado en tu servidor y no en el de otro. El registro es cerrado, por invitación.
Los importes se guardan como enteros en centavos y se reparten por el método del resto mayor, así la suma de las partes da siempre exactamente el total del gasto. No se pierde ni se inventa un centavo.
Cargar un gasto, registrar un pago, y sumar a alguien a los gastos que ya estaban cargados.
Adentro de un grupo: quién le debe a quién, ya simplificado, y el botón para saldar de un click.
CONTEXT.mdexplica por qué las cosas son como son y qué se decidió no hacer.AGENTS.mdes la guía para trabajar en el código.
- Grupos con moneda propia, ícono y archivado.
- Gastos con cuatro formas de dividir: partes iguales, montos exactos, porcentajes y partes (ej. 2 partes para quien viene en pareja).
- Varios pagadores en un mismo gasto.
- Balances por grupo y consolidados por persona.
- Simplificación de deudas opcional por grupo: en vez de A→B y B→C, sugiere A→C directo.
- Pagos (settlements) para saldar cuentas, con sugerencias precargadas.
- Comentarios en cada gasto y feed de actividad por grupo.
- Invitaciones por link: de un solo uso, o ilimitado para mandar al grupo de WhatsApp, con vista previa al compartirlo.
- Modo claro y oscuro automático, responsive.
| Framework | Next.js 16 (App Router, Server Actions) |
| Lenguaje | TypeScript |
| Base | PostgreSQL 17 + Prisma 7 (driver adapter pg) |
| Estilos | Tailwind CSS 4 |
| Auth | Sesiones propias en cookie httpOnly + bcrypt |
| Deploy | Docker (multi-stage) → Coolify |
npm install
cp .env.example .env # editá los valores
docker compose -f docker-compose.dev.yml up -d # Postgres en el 5433
npx prisma migrate deploy # crea las tablas
npm run db:seed # datos de ejemplo (opcional)
npm run devAbrí http://localhost:3000.
Si corriste el seed, entrás con el email de BOOTSTRAP_ADMIN_EMAIL y la
contraseña vaquita1234. Ojo: el seed borra toda la base.
| Comando | Qué hace |
|---|---|
npm run dev |
Servidor de desarrollo |
npm run build |
prisma generate + build de producción. Es el chequeo de tipos: no hay tests ni linter. |
npm run db:migrate |
Crea una migración nueva a partir del schema |
npm run db:deploy |
Aplica migraciones pendientes (producción) |
npm run db:studio |
Prisma Studio para mirar la base |
npm run db:seed |
Carga datos de ejemplo (destructivo) |
cp .env.example .env # completá AUTH_SECRET, APP_URL, POSTGRES_PASSWORD…
docker compose up -dLevanta la app y su base. Las migraciones se aplican solas al arrancar. Poné un reverse proxy adelante (Caddy, nginx, Traefik) para el dominio y el certificado.
Cuando esté arriba, entrá a tu dominio y creá tu cuenta: ver Tu primera cuenta.
Hay dos caminos. El de abajo crea la base como recurso propio de Coolify, que es lo que te habilita sus backups automáticos — para una app donde el dato es el producto, eso solo justifica los dos minutos extra.
Si preferís levantar todo de una, usá Build Pack Docker Compose con
Docker Compose Location en /docker-compose.coolify.yml, y cargá las
variables de la tabla del paso 3 salvo DATABASE_URL (esa se arma sola) más
POSTGRES_PASSWORD. Los backups en ese caso corren por tu cuenta.
-
Base de datos: en tu proyecto de Coolify,
+ New→Database→PostgreSQL 17. Anotá la connection string interna. -
Aplicación:
+ New→Public/Private Repository→github.com/eerginos/vaquita.- Build Pack: Dockerfile
- Port: 3000
- Health check path:
/api/salud
-
Variables de entorno:
Variable Valor DATABASE_URLLa connection string interna del Postgres de Coolify AUTH_SECRETopenssl rand -hex 32APP_URLLa URL pública, por ejemplo https://vaquita.tudominio.comBOOTSTRAP_ADMIN_EMAILTu email La zona horaria no es una variable: se elige desde
/adminy queda guardada en la base.TZsólo se usa como valor inicial la primera vez. -
Dominio: poné el tuyo, el mismo que
APP_URL. Coolify saca el certificado con Let's Encrypt solo. -
Deploy. El contenedor corre
prisma migrate deployal arrancar, así que las tablas se crean solas. Cuando termine, creá tu cuenta: ver Tu primera cuenta.
Como el repo es público no hay integración de GitHub App, así que el disparador va por webhook y se configura una sola vez:
- En Coolify, en la aplicación → pestaña Webhooks → fila GitHub, dentro de Manual Git Webhooks. Copiá la URL y revelá el secret con el ojito. No uses el Deploy Webhook de arriba: ese pide un token de API y desde GitHub te va a devolver 401.
- El botón Webhook Configuration on GitHub te lleva derecho a la pantalla del
repo. Pegá la URL y el secret, poné content type
application/jsony dejá sólo el eventopush.
Para verificar, mirá Recent Deliveries en el webhook de GitHub: tilde verde es que Coolify contestó. Si da timeout, tu Coolify no es accesible desde internet.
Entrá a tu dominio. Como todavía no hay nadie, la pantalla de ingreso te ofrece
Creá la primera — no hace falta que te acuerdes de ninguna ruta. Registrate
con el mismo email que pusiste en BOOTSTRAP_ADMIN_EMAIL.
Esa cuenta se crea sin invitación y queda como administradora. Es la única que puede hacerlo: apenas existe, el link desaparece y el registro queda cerrado. De ahí en adelante se entra sólo por invitación.
Después conviene pasar por /admin y elegir la zona horaria, que es de lo que
depende que un gasto cargado de noche quede con la fecha correcta.
No hay envío de mails (a propósito: una dependencia menos). Todo va por link:
- Alguien nuevo →
/admin→ Invitar a alguien nuevo → generás el link y se lo pasás por WhatsApp. Vence a los 14 días, se usa una sola vez. - Alguien nuevo directo a un grupo → configuración del grupo → Invitar gente nueva. Al crear la cuenta ya entra al grupo.
- Alguien que ya tiene cuenta → configuración del grupo → Sumar gente que ya tiene cuenta.
- Se olvidó la contraseña →
/admin→ Restablecer una contraseña → generás un link que vence en 24 horas y se lo pasás.
- Las contraseñas se guardan con bcrypt (12 rondas).
- El login tiene freno a la fuerza bruta: 5 intentos fallidos por cuenta y 20 por IP en una ventana de 15 minutos, con esperas que crecen (1, 2, 5, 15, 30 minutos). El contador es por IP y email, así nadie puede dejarte afuera de tu propia cuenta a propósito.
- El token de sesión viaja en una cookie
httpOnly+SameSite=Lax; en la base sólo se guarda su HMAC-SHA256 conAUTH_SECRET, nunca el token. - Cambiar la contraseña cierra todas las demás sesiones.
AUTH_SECRETes obligatorio y tiene que tener al menos 16 caracteres: si cambia, todas las sesiones y los links de reset dejan de valer.- Todas las Server Actions revalidan la membresía al grupo antes de tocar nada.
Si encontrás un agujero, no abras un issue: está todo en SECURITY.md.
Multi-moneda con conversión, recibos adjuntos, gastos recurrentes y
notificaciones por email. La estructura ya está preparada: los gastos guardan
currency propio y los balances se agrupan por moneda.
MIT — hacé lo que quieras con esto, manteniendo el aviso de copyright. Se entrega sin garantías: si lo desplegás, la responsabilidad de que los números estén bien es tuya.
